Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
121 KB
Referenced Files
None
Subscribers
None
diff --git a/plugins/tasklist/skins/larry/sprites.png b/plugins/tasklist/skins/larry/sprites.png
index 5224f6f7..b20b2db0 100644
Binary files a/plugins/tasklist/skins/larry/sprites.png and b/plugins/tasklist/skins/larry/sprites.png differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index caa50676..70ab8da9 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -1,851 +1,885 @@
/**
* Roundcube Taklist plugin styles for skin "Larry"
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
#taskbar a.button-tasklist span.button-inner {
background-image: url(buttons.png);
background-position: 0 0;
}
#taskbar a.button-tasklist:hover span.button-inner,
#taskbar a.button-tasklist.button-selected span.button-inner {
background-position: 0 -26px;
}
ul.toolbarmenu li span.icon.taskadd {
background-image: url(buttons.png);
background-position: -4px -90px;
}
div.uidialog {
display: none;
}
body.attachmentwin #mainscreen {
top: 60px;
}
body.attachmentwin #topnav .topright {
margin-top: 20px;
}
#sidebar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 240px;
}
.tasklistview #searchmenulink {
width: 15px;
}
#tagsbox {
position: absolute;
top: 42px;
left: 0;
width: 100%;
height: 242px;
}
#tasklistsbox {
position: absolute;
top: 300px;
left: 0;
width: 100%;
bottom: 0px;
}
#taskselector {
- margin: -4px 0 0;
+ margin: -4px 40px 0 0;
padding: 0;
}
#taskselector li {
display: inline-block;
position: relative;
font-size: 90%;
padding-right: 0.3em;
}
#tagslist li,
#taskselector li a {
display: inline-block;
color: #004458;
min-width: 4em;
padding: 0.2em 0.6em 0.3em 0.6em;
text-align: center;
text-decoration: none;
border: 1px solid #eee;
border-color: transparent;
}
#taskselector li:first-child {
border-top: 0;
border-radius: 4px 4px 0 0;
}
#taskselector li:last-child {
border-bottom: 0;
border-radius: 0 0 4px 4px;
}
#taskselector li.overdue a {
color: #b72a2a;
font-weight: bold;
}
#taskselector li.inactive a {
color: #97b3bf;
}
#tagslist li.selected,
#taskselector li.selected a {
color: #fff;
background: #005d76;
background: -moz-linear-gradient(top, #005d76 0%, #004558 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#005d76), color-stop(100%,#004558));
background: -o-linear-gradient(top, #005d76 0%, #004558 100%);
background: -ms-linear-gradient(top, #005d76 0%, #004558 100%);
background: linear-gradient(top, #005d76 0%, #004558 100%);
box-shadow: inset 0 1px 1px 0 #003645;
-o-box-shadow: inset 0 1px 1px 0 #003645;
-webkit-box-shadow: inset 0 1px 1px 0 #003645;
-moz-box-shadow: inset 0 1px 1px 0 #003645;
border-color: #003645;
border-radius: 9px;
text-shadow: none;
outline: none;
}
#taskselector li .count {
display: none;
position: absolute;
top: -18px;
right: 5px;
min-width: 1.8em;
padding: 2px 4px;
background: #004558;
background: -moz-linear-gradient(top, #005d76 0%, #004558 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#005d76), color-stop(100%,#004558));
background: -o-linear-gradient(top, #005d76 0%, #004558 100%);
background: -ms-linear-gradient(top, #005d76 0%, #004558 100%);
background: linear-gradient(top, #005d76 0%, #004558 100%);
box-shadow: 0 1px 2px 0 rgba(24,24,24,0.6);
color: #fff;
border-radius: 3px;
text-align: center;
font-weight: bold;
font-size: 80%;
text-shadow: none;
}
#taskselector li .count:after {
content: "";
position: absolute;
bottom: -5px;
left: 50%;
margin-left: -5px;
border-style: solid;
border-width: 5px 5px 0;
border-color: #004558 transparent;
/* reduce the damage in FF3.0 */
display: block;
width: 0;
}
#taskselector li.overdue .count {
background: #ff3800;
}
#taskselector li.overdue .count:after {
border-color: #ff3800 transparent;
}
#tagslist {
padding: 0;
margin: 6px;
list-style: none;
}
#tagslist li {
display: inline-block;
color: #004458;
margin-right: 0.5em;
margin-bottom: 0.4em;
min-width: 1.2em;
cursor: pointer;
}
#tasklists li {
margin: 0;
height: 20px;
padding: 6px 8px 2px;
display: block;
position: relative;
white-space: nowrap;
}
#tasklists li.virtual {
padding-top: 4px;
height: 16px;
}
#tasklists li label {
display: block;
}
#tasklists li span.listname {
display: block;
cursor: default;
padding-bottom: 2px;
padding-right: 30px;
margin-right: 20px;
color: #004458;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background: url(sprites.png) right 20px no-repeat;
}
#tasklists li span.handle {
display: none;
}
#tasklists li.selected span.listname {
font-weight: bold;
}
#tasklists li.readonly span.listname {
background-position: right -142px;
}
#tasklists li.other span.listname {
background-position: right -160px;
}
#tasklists li.other.readonly span.listname {
background-position: right -178px;
}
#tasklists li.shared span.listname {
background-position: right -196px;
}
#tasklists li.shared.readonly span.listname {
background-position: right -214px;
}
#tasklists li.virtual span.listname {
color: #aaa;
}
#tasklists li input {
position: absolute;
top: 5px;
right: 5px;
}
#mainview-right {
position: absolute;
top: 0;
left: 256px;
right: 0;
bottom: 0;
}
#taskstoolbar {
position: absolute;
top: -6px;
left: 0;
width: 100%;
height: 40px;
white-space: nowrap;
}
#taskstoolbar a.button.newtask {
background-image: url(buttons.png);
background-position: center -53px;
}
#quickaddbox {
position: absolute;
top: 2px;
left: 0;
width: 60%;
height: 32px;
white-space: nowrap;
}
#quickaddinput {
width: 85%;
margin: 0;
padding: 3px 8px;
height: 18px;
background: #f1f1f1;
background: rgba(255, 255, 255, 0.7);
border-color: #a3a3a3;
font-weight: bold;
}
#quickaddbox .button {
margin-left: 5px;
padding: 3px 10px;
font-weight: bold;
}
#tasksview {
position: absolute;
top: 42px;
left: 0;
right: 0;
bottom: 0;
padding-bottom: 28px;
background: rgba(255, 255, 255, 0.2);
overflow: visible;
}
#message.statusbar {
border-top: 1px solid #c3c3c3;
}
#tasksview .scroller {
position: absolute;
left: 0;
top: 35px;
width: 100%;
bottom: 28px;
overflow: auto;
}
#tasksview .buttonbar {
color: #777;
background: #eee;
background: -moz-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#eee), color-stop(100%,#dfdfdf));
background: -o-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: -ms-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: linear-gradient(top, #eee 0%, #dfdfdf 100%);
border-bottom: 1px solid #ccc;
+ position: relative;
+}
+
+#tasksview .buttonbar .buttonbar-right {
+ position: absolute;
+ top: 6px;
+ right: 8px;
+}
+
+.buttonbar-right .listmenu {
+ display: inline-block;
+ cursor: pointer;
+}
+
+.buttonbar-right .listmenu .inner {
+ display: inline-block;
+ height: 18px;
+ width: 20px;
+ padding: 0;
+ background: url(sprites.png) 0 -237px no-repeat;
+ text-indent: -5000px;
}
#thelist {
padding: 0;
margin: 1em;
list-style: none;
}
#listmessagebox {
display: none;
font-size: 14px;
color: #666;
margin: 1.5em;
text-shadow: 0px 1px 1px #fff;
text-align:center;
}
.taskitem {
position: relative;
display: block;
margin-bottom: 3px;
}
.taskitem.dragging {
opacity: 0.5;
}
.taskitem .childtasks {
position: relative;
padding: 0;
margin: 3px 0 0 20px;
list-style: none;
}
.taskitem .childtoggle {
display: none;
position: absolute;
top: 4px;
left: -5px;
padding: 2px;
font-size: 10px;
color: #727272;
cursor: pointer;
width: 14px;
height: 14px;
background: url(sprites.png) -2px -80px no-repeat;
text-indent: -1000px;
overflow: hidden;
}
.taskitem .childtoggle.collapsed {
background-position: -18px -81px;
}
.taskhead {
position: relative;
margin-left: 14px;
padding: 4px 5px 3px 5px;
border: 1px solid #fff;
border-radius: 5px;
background: #fff;
-webkit-box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
-moz-box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
padding-right: 26em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;
}
.taskhead.droptarget {
border-color: #4787b1;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
}
.taskhead .complete {
margin: -1px 1em 0 0;
}
.taskhead .title {
font-size: 12px;
}
.taskhead .flagged {
display: inline-block;
visibility: hidden;
width: 16px;
height: 16px;
background: url(sprites.png) -2px -3px no-repeat;
margin: -3px 1em 0 0;
vertical-align: middle;
cursor: pointer;
}
.taskhead:hover .flagged {
visibility: visible;
}
.taskhead.flagged .flagged {
visibility: visible;
background-position: -2px -23px;
}
.taskhead .tags {
display: block;
position: absolute;
top: 3px;
right: 10em;
max-width: 14em;
height: 16px;
overflow: hidden;
padding-top: 1px;
text-align: right;
}
.taskhead .tags .tag {
font-size: 85%;
background: #d9ecf4;
border: 1px solid #c2dae5;
border-radius: 4px;
padding: 1px 7px;
margin-right: 3px;
}
.taskhead .date {
position: absolute;
top: 4px;
right: 30px;
text-align: right;
cursor: pointer;
}
.taskhead.nodate .date {
color: #ddd;
}
.taskhead.overdue .date {
color: #d00;
}
.taskhead.nodate:hover .date {
color: #999;
}
.taskhead .date input {
padding: 1px 2px;
border: 1px solid #ddd;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
outline: none;
text-align: right;
width: 6em;
font-size: 11px;
}
.taskhead .actions,
.taskhead .delete {
display: block;
visibility: hidden;
position: absolute;
top: 3px;
right: 6px;
width: 18px;
height: 18px;
background: url(sprites.png) 0 -80px no-repeat;
text-indent: -1000px;
overflow: hidden;
cursor: pointer;
}
.taskhead .delete {
background-position: 0 -40px;
}
.taskhead:hover .actions,
.taskhead:hover .delete {
visibility: visible;
}
.taskhead.complete {
opacity: 0.6;
}
.taskhead.complete .title {
text-decoration: line-through;
}
.taskhead .progressbar {
position: absolute;
bottom: 1px;
left: 6px;
right: 6px;
height: 2px;
}
.taskhead.complete .progressbar {
display: none;
}
.taskhead .progressvalue {
height: 1px;
background: rgba(1, 124, 180, 0.2);
border-top: 1px solid #219de6;
}
-ul.toolbarmenu li span.add {
+ul.toolbarmenu li span.add,
+ul.toolbarmenu li span.expand,
+ul.toolbarmenu li span.collapse {
background-image: url(sprites.png);
+}
+
+ul.toolbarmenu li span.add {
background-position: 0 -100px;
}
+ul.toolbarmenu li span.expand {
+ background-position: 0 -258px;
+}
+
+ul.toolbarmenu li span.collapse {
+ background-position: 0 -280px;
+}
+
ul.toolbarmenu li span.delete {
background-position: 0 -1508px;
}
.taskitem-draghelper {
/*
width: 32px;
height: 26px;
*/
background: #444;
border: 1px solid #555;
border-radius: 4px;
box-shadow: 0 2px 6px 0 #333;
-moz-box-shadow: 0 2px 6px 0 #333;
-webkit-box-shadow: 0 2px 6px 0 #333;
-o-box-shadow: 0 2px 6px 0 #333;
z-index: 5000;
padding: 2px 10px;
font-size: 20px;
color: #ccc;
opacity: 0.92;
filter: alpha(opacity=90);
text-shadow: 0px 1px 1px #333;
}
#rootdroppable {
display: none;
position: absolute;
top: 36px;
left: 1em;
right: 1em;
height: 5px;
background: #ddd;
border-radius: 3px;
}
#rootdroppable.droptarget {
background: #4787b1;
box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
}
/*** task edit form ***/
#taskedit,
#taskshow {
display:none;
}
#taskedit {
position: relative;
top: -1.5em;
padding: 0.5em 0.1em;
margin: 0 -0.2em;
}
#taskshow h2 {
margin-top: -0.5em;
}
#taskshow label {
color: #999;
}
#task-parent-title {
position: relative;
top: -0.6em;
}
a.morelink {
font-size: 90%;
color: #0069a6;
text-decoration: none;
outline: none;
}
a.morelink:hover {
text-decoration: underline;
}
#taskedit .ui-tabs-panel {
min-height: 24em;
}
#taskeditform input.text,
#taskeditform textarea {
width: 97%;
}
#taskeditform .formbuttons {
margin: 0.5em 0;
}
#taskedit-attachments {
margin: 0.6em 0;
}
#taskedit-attachments ul li {
display: block;
color: #333;
font-weight: bold;
padding: 8px 4px 3px 30px;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
white-space: nowrap;
}
#taskedit-attachments ul li a.file {
padding: 0;
}
#taskedit-attachments-form {
margin-top: 1em;
padding-top: 0.8em;
border-top: 2px solid #fafafa;
}
div.form-section {
position: relative;
margin-top: 0.2em;
margin-bottom: 0.8em;
}
.form-section label {
display: inline-block;
min-width: 7em;
padding-right: 0.5em;
margin-bottom: 0.3em;
}
label.block {
display: block;
margin-bottom: 0.3em;
}
#taskedit-completeness-slider {
display: inline-block;
margin-left: 2em;
width: 30em;
height: 0.8em;
border: 1px solid #ccc;
}
#taskedit-tagline {
width: 97%;
}
#taskedit .droptarget {
background-image: url(../../../../skins/larry/images/filedrop.png) !important;
background-position: center bottom !important;
background-repeat: no-repeat !important;
}
#taskedit .droptarget.hover,
#taskedit .droptarget.active {
border-color: #019bc6;
box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-moz-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-webkit-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
-o-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
}
#taskedit .droptarget.hover {
background-color: #d9ecf4;
box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
}
#task-attachments .attachmentslist li {
float: left;
margin-right: 1em;
}
#task-attachments .attachmentslist li a {
outline: none;
}
/**
* Styles of the tagedit inputsforms
*/
.tagedit-list {
width: 100%;
margin: 0;
padding: 4px 4px 0 5px;
overflow: auto;
min-height: 26px;
background: #fff;
border: 1px solid #b2b2b2;
border-radius: 4px;
box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-moz-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-webkit-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-o-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
}
.tagedit-list li.tagedit-listelement {
list-style-type: none;
float: left;
margin: 0 4px 4px 0;
padding: 0;
}
/* New Item input */
.tagedit-list li.tagedit-listelement-new input {
border: 0;
height: 100%;
padding: 4px 1px;
width: 15px;
background: #fff;
border-radius: 0;
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
-o-box-shadow: none;
}
.tagedit-list li.tagedit-listelement-new input:focus {
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
-o-box-shadow: none;
outline: none;
}
.tagedit-list li.tagedit-listelement-new input.tagedit-input-disabled {
display: none;
}
/* Item that is put to the List */
.form-section span.tag-element,
.tagedit-list li.tagedit-listelement-old {
padding: 3px 0 1px 6px;
background: #ddeef5;
background: -moz-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#edf6fa), color-stop(100%,#d6e9f3));
background: -o-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: -ms-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
border: 1px solid #c2dae5;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
color: #0d5165;
}
.form-section span.tag-element {
margin-right: 0.6em;
padding: 2px 6px;
/* cursor: pointer; */
}
.form-section span.tag-element.inherit {
color: #666;
background: #f2f2f2;
border-color: #ddd;
}
.tagedit-list li.tagedit-listelement-old a.tagedit-close,
.tagedit-list li.tagedit-listelement-old a.tagedit-break,
.tagedit-list li.tagedit-listelement-old a.tagedit-delete,
.tagedit-list li.tagedit-listelement-old a.tagedit-save {
text-indent: -2000px;
display: inline-block;
position: relative;
top: -1px;
width: 16px;
height: 16px;
margin: 0 2px 0 6px;
background: url(sprites.png) -2px -122px no-repeat;
cursor: pointer;
}
/** Special hacks for IE7 **/
/** They need to be in this file to also affect the task-create dialog embedded in mail view **/
html.ie7 #taskedit-completeness-slider {
display: inline;
}
html.ie7 .form-section span.tag-element,
html.ie7 .tagedit-list li.tagedit-listelement-old {
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#edf6fa', endColorstr='#d6e9f3', GradientType=0);
}
html.ie7 .tagedit-list li.tagedit-listelement span {
position: relative;
top: -3px;
}
html.ie7 .tagedit-list li.tagedit-listelement-old a.tagedit-close {
left: 5px;
}
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index 9196d028..b85f7fbd 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -1,154 +1,165 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
<!--[if lte IE 8]><link rel="stylesheet" type="text/css" href="/this/iehacks.css" /><![endif]-->
</head>
<body class="tasklistview noscroll">
<roundcube:include file="/includes/header.html" />
<div id="mainscreen">
<div id="sidebar">
<div id="taskstoolbar" class="toolbar">
<roundcube:button command="newtask" type="link" class="button newtask disabled" classAct="button newtask" classSel="button newtask pressed" label="tasklist.newtask" title="tasklist.newtask" />
<roundcube:container name="toolbar" id="taskstoolbar" />
</div>
<div id="tagsbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="tasklist.tags" id="taglist" /></h2>
<div class="scroller">
<roundcube:object name="plugin.tagslist" id="tagslist" />
</div>
</div>
<div id="tasklistsbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="tasklist.lists" /></h2>
<div class="scroller withfooter">
<roundcube:object name="plugin.tasklists" id="tasklists" class="listing" />
</div>
<div class="boxfooter">
<roundcube:button command="list-create" type="link" title="tasklist.createlist" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="tasklistoptionslink" id="tasklistoptionsmenulink" type="link" title="tasklist.listactions" class="listbutton groupactions" onclick="UI.show_popup('tasklistoptionsmenu', undefined, { above:true });return false" innerClass="inner" content="&#9881;" />
</div>
</div>
</div>
<div id="mainview-right">
<div id="quickaddbox">
<roundcube:object name="plugin.quickaddform" />
</div>
<div id="quicksearchbar">
<roundcube:object name="plugin.searchform" id="quicksearchbox" />
<a id="searchmenulink" class="iconbutton searchoptions" > </a>
<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
</div>
<div id="tasksview" class="uibox">
<div class="boxtitle buttonbar">
<ul id="taskselector">
<li class="all selected"><a href="#all"><roundcube:label name="tasklist.all" /><span class="count"></span></a></li>
<li class="overdue inactive"><a href="#overdue"><roundcube:label name="tasklist.overdue" /><span class="count"></span></a></li>
<li class="flagged"><a href="#flagged"><roundcube:label name="tasklist.flagged" /><span class="count"></span></a></li>
<li class="today"><a href="#today"><roundcube:label name="tasklist.today" /><span class="count"></span></a></li>
<li class="tomorrow"><a href="#tomorrow"><roundcube:label name="tasklist.tomorrow" /><span class="count"></span></a></li>
<li class="week"><a href="#week"><roundcube:label name="tasklist.next7days" /></a></li>
<li class="later"><a href="#later"><roundcube:label name="tasklist.later" /></a></li>
<li class="nodate"><a href="#nodate"><roundcube:label name="tasklist.nodate" ucfirst="true" /></a></li>
<li class="complete"><a href="#complete"><roundcube:label name="tasklist.complete" /><span class="count"></span></a></li>
</ul>
+
+ <div class="buttonbar-right">
+ <roundcube:button name="taskviewmenulink" id="taskviewmenulink" type="link" title="tasklist.viewoptions" class="listmenu viewoptions" onclick="UI.show_popup('taskviewmenu');return false" innerClass="inner" content="&#9881;" />
+ </div>
</div>
<div class="scroller">
<roundcube:object name="plugin.tasks" id="thelist" />
<div id="listmessagebox"></div>
</div>
<div id="rootdroppable"></div>
<roundcube:object name="message" id="message" class="statusbar" />
</div>
</div>
</div>
<div id="taskitemmenu" class="popupmenu">
<ul class="toolbarmenu iconized">
<li><roundcube:button name="edit" type="link" onclick="rctasks.edit_task(rctasks.selected_task.id, 'edit'); return false" label="edit" class="icon active" innerclass="icon edit" /></li>
<li><roundcube:button name="delete" type="link" onclick="rctasks.delete_task(rctasks.selected_task.id); return false" label="delete" class="icon active" innerclass="icon delete" /></li>
<li><roundcube:button name="addchild" type="link" onclick="rctasks.add_childtask(rctasks.selected_task.id); return false" label="tasklist.addsubtask" class="icon active" innerclass="icon add" /></li>
</ul>
</div>
<div id="tasklistoptionsmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><roundcube:button command="list-edit" label="edit" classAct="active" /></li>
<li><roundcube:button command="list-remove" label="delete" classAct="active" /></li>
<!--<li><roundcube:button command="list-import" label="tasklist.import" classAct="active" /></li>-->
<roundcube:if condition="env:tasklist_driver == 'kolab'" />
<li><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
<roundcube:endif />
</ul>
</div>
+<div id="taskviewmenu" class="popupmenu">
+ <ul class="toolbarmenu">
+ <li><roundcube:button command="expand-all" label="expand-all" class="icon" classAct="icon active" innerclass="icon expand" /></li>
+ <li><roundcube:button command="collapse-all" label="collapse-all" class="icon" classAct="icon active" innerclass="icon collapse" /></li>
+ </ul>
+</div>
+
<div id="taskshow">
<div class="form-section" id="task-parent-title"></div>
<div class="form-section">
<h2 id="task-title"></h2>
</div>
<div id="task-description" class="form-section">
</div>
<div id="task-tags" class="form-section">
<label><roundcube:label name="tasklist.tags" /></label>
<span class="task-text"></span>
</div>
<div id="task-start" class="form-section">
<label><roundcube:label name="tasklist.start" /></label>
<span class="task-text"></span>
<span id="task-starttime"></span>
</div>
<div id="task-date" class="form-section">
<label><roundcube:label name="tasklist.datetime" /></label>
<span class="task-text"></span>
<span id="task-time"></span>
</div>
<div id="task-alarm" class="form-section">
<label><roundcube:label name="tasklist.alarms" /></label>
<span class="task-text"></span>
</div>
<div id="task-list" class="form-section">
<label><roundcube:label name="tasklist.list" /></label>
<span class="task-text"></span>
</div>
<div id="task-completeness" class="form-section">
<label><roundcube:label name="tasklist.complete" /></label>
<span class="task-text"></span>
</div>
<div id="task-attachments" class="form-section">
<label><roundcube:label name="attachments" /></label>
<div class="task-text"></div>
</div>
</div>
<roundcube:include file="/templates/taskedit.html" />
<div id="tasklistform" class="uidialog">
<roundcube:object name="plugin.tasklist_editform" />
</div>
<script type="text/javascript">
// UI startup
var UI = new rcube_mail_ui();
$(document).ready(function(e){
UI.init();
new rcube_splitter({ id:'taskviewsplitter', p1:'#sidebar', p2:'#mainview-right',
orientation:'v', relative:true, start:240, min:180, size:16, offset:2 }).init();
});
</script>
</body>
</html>
\ No newline at end of file
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 631494c6..742f23a7 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -1,1786 +1,1813 @@
/**
* Client scripts for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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_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
};
/* private vars */
var selector = 'all';
var tagsfilter = [];
var filtermask = FILTER_MASK_ALL;
var loadstate = { filter:-1, lists:'', search:null };
var idcount = 0;
var saving_lock;
var ui_loading;
var taskcounts = {};
var listindex = [];
var listdata = {};
var tags = [];
var draghelper;
var search_request;
var search_query;
var completeness_slider;
var me = this;
// general datepicker settings
var datepicker_settings = {
// translate from PHP format to datepicker format
dateFormat: settings['date_format'].replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'),
firstDay : settings['first_day'],
// dayNamesMin: settings['days_short'],
// monthNames: settings['months'],
// monthNamesShort: settings['months'],
changeMonth: false,
showOtherMonths: true,
selectOtherMonths: true
};
var extended_datepicker_settings;
/* public members */
this.tasklists = rcmail.env.tasklists;
this.selected_task;
this.selected_list;
/* public methods */
this.init = init;
this.edit_task = task_edit_dialog;
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_remove = list_remove;
this.list_edit_dialog = list_edit_dialog;
this.unlock_saving = unlock_saving;
/* imports */
var Q = this.quote_html;
var text2html = this.text2html;
var event_date_text = this.event_date_text;
var parse_datetime = this.parse_datetime;
var date2unixtime = this.date2unixtime;
var fromunixtime = this.fromunixtime;
var init_alarms_edit = this.init_alarms_edit;
/**
* initialize the tasks UI
*/
function init()
{
// initialize task list selectors
for (var id in me.tasklists) {
if ((li = rcmail.get_folder_li(id, 'rcmlitasklist'))) {
init_tasklist_li(li, id);
}
if (me.tasklists[id].editable && !me.selected_list) {
me.selected_list = id;
rcmail.enable_command('addtask', true);
$(li).click();
}
}
// register server callbacks
rcmail.addEventListener('plugin.data_ready', data_ready);
rcmail.addEventListener('plugin.refresh_task', update_taskitem);
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.reload_data', function(){ list_tasks(null); });
rcmail.addEventListener('plugin.unlock_saving', unlock_saving);
rcmail.addEventListener('requestrefresh', before_refresh);
// start loading tasks
fetch_counts();
list_tasks();
// register event handlers for UI elements
$('#taskselector a').click(function(e){
if (!$(this).parent().hasClass('inactive'))
list_tasks(this.href.replace(/^.*#/, ''));
return false;
});
// quick-add a task
$(rcmail.gui_objects.quickaddform).submit(function(e){
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');
render_task(rec);
$('#listmessagebox').hide();
}
// clear form
this.reset();
return false;
}).find('input[type=text]').placeholder(rcmail.gettext('createnewtask','tasklist'));
// click-handler on tags list
$(rcmail.gui_objects.tagslist).click(function(e){
if (e.target.nodeName != 'LI')
return false;
var item = $(e.target),
tag = item.data('value');
// reset selection on regular clicks
var index = $.inArray(tag, tagsfilter);
var shift = e.shiftKey || e.ctrlKey || e.metaKey;
if (!shift) {
if (tagsfilter.length > 1)
index = -1;
$('li', this).removeClass('selected');
tagsfilter = [];
}
// add tag to filter
if (index < 0) {
item.addClass('selected');
tagsfilter.push(tag);
}
else if (shift) {
item.removeClass('selected');
var a = tagsfilter.slice(0,index);
tagsfilter = a.concat(tagsfilter.slice(index+1));
}
list_tasks();
// clear text selection in IE after shift+click
if (shift && document.selection)
document.selection.empty();
e.preventDefault();
return false;
})
.mousedown(function(e){
// disable content selection with the mouse
e.preventDefault();
return false;
});
// click-handler on task list items (delegate)
$(rcmail.gui_objects.resultlist).click(function(e){
var item = $(e.target);
var 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');
// 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();
$(e.target).toggleClass('collapsed').html(rec.collapsed ? '&#9654;' : '&#9660;');
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;
rec.complete = e.target.checked ? 1 : 0;
li.toggleClass('complete');
save_task(rec, 'edit');
return true;
case 'flagged':
if (rcmail.busy)
return false;
rec.flagged = rec.flagged ? 0 : 1;
li.toggleClass('flagged');
save_task(rec, 'edit');
break;
case 'date':
if (rcmail.busy)
return false;
var link = $(e.target).html(''),
input = $('<input type="text" size="10" />').appendTo(link).val(rec.date || '')
input.datepicker($.extend({
onClose: function(dateText, inst) {
if (dateText != (rec.date || '')) {
rec.date = dateText;
save_task(rec, 'edit');
}
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) {
menu.hide();
}
else {
pos = ref.offset();
pos.top += ref.outerHeight();
pos.left += ref.width() - menu.outerWidth();
menu.css({ top:pos.top+'px', left:pos.left+'px' }).show();
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);
break;
}
return false;
})
.dblclick(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();
}
});
// handle global document clicks: close popup menus
$(document.body).click(clear_popups);
// 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', '')
.click(function(e){
$(input).datepicker('setDate', null).datepicker('hide');
});
}, 1);
}
}, datepicker_settings);
}
/**
* initialize task edit form elements
*/
function init_taskedit()
{
$('#taskedit').tabs();
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 alarm fields
init_alarms_edit('#taskedit');
$('#taskedit-date, #taskedit-startdate').datepicker(datepicker_settings);
$('a.edit-nodate').click(function(){
var sel = $(this).attr('rel');
if (sel) $(sel).val('');
return false;
});
}
/**
* 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)
{
if (rcmail.busy)
return;
if (sel && filter_masks[sel] !== undefined) {
filtermask = filter_masks[sel];
selector = sel;
}
var active = active_lists(),
basefilter = filtermask == FILTER_MASK_COMPLETE ? FILTER_MASK_COMPLETE : FILTER_MASK_ALL,
reload = 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');
$('#taskselector li.'+selector).addClass('selected');
}
/**
* 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();
}
/**
* 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);
}
render_tasklist();
append_tags(response.tags || []);
rcmail.set_busy(false, 'loading', ui_loading);
}
/**
*
*/
function render_tasklist()
{
// clear display
var id, rec,
count = 0,
cache = {},
msgbox = $('#listmessagebox').hide(),
list = $(rcmail.gui_objects.resultlist).html('');
for (var i=0; i < listindex.length; i++) {
id = listindex[i];
rec = listdata[id];
if (match_filter(rec, cache)) {
render_task(rec);
count++;
}
}
fix_tree_toggles();
if (!count)
msgbox.html(rcmail.gettext('notasksfound','tasklist')).show();
}
/**
* 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(collapsed ? '&#9654;' : '&#9660;');
+
+ // 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 });
+ }
+ }
+
/**
*
*/
function append_tags(taglist)
{
// find new tags
var newtags = [];
for (var i=0; i < taglist.length; i++) {
if ($.inArray(taglist[i], tags) < 0)
newtags.push(taglist[i]);
}
tags = tags.concat(newtags);
// append new tags to tag cloud
$.each(newtags, function(i, tag){
$('<li>').attr('rel', tag).data('value', tag).html(Q(tag)).appendTo(rcmail.gui_objects.tagslist);
});
// re-sort tags list
$(rcmail.gui_objects.tagslist).children('li').sortElements(function(a,b){
return $.text([a]).toLowerCase() > $.text([b]).toLowerCase() ? 1 : -1;
});
}
/**
*
*/
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+/, '');
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)
{
// handle a list of task records
if ($.isArray(rec)) {
$.each(rec, function(i,r){ update_taskitem(r); });
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 (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);
}
}
if (list.active)
render_task(rec, oldid);
else
$('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).remove();
append_tags(rec.tags || []);
fix_tree_toggles();
}
/**
* Submit the given (changed) task record to the server
*/
function save_task(rec, action)
{
if (!rcmail.busy) {
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('tasks/task', { action:action, t:rec, filter:filtermask });
$('button.ui-button:ui-button').button('option', 'disabled', rcmail.busy);
return true;
}
return false;
}
/**
* Remove saving lock and free the UI for new input
*/
function unlock_saving()
{
if (saving_lock) {
rcmail.set_busy(false, null, saving_lock);
$('button.ui-button:ui-button').button('option', 'disabled', false);
}
}
/**
* Render the given task into the tasks list
*/
function render_task(rec, replace)
{
var tags_html = '';
for (var j=0; rec.tags && j < rec.tags.length; j++)
tags_html += '<span class="tag">' + Q(rec.tags[j]) + '</span>';
var div = $('<div>').addClass('taskhead').html(
'<div class="progressbar"><div class="progressvalue" style="width:' + (rec.complete * 100) + '%"></div></div>' +
'<input type="checkbox" name="completed[]" value="1" class="complete" ' + (rec.complete == 1.0 ? 'checked="checked" ' : '') + '/>' +
'<span class="flagged"></span>' +
'<span class="title">' + text2html(Q(rec.title)) + '</span>' +
'<span class="tags">' + tags_html + '</span>' +
'<span class="date">' + Q(rec.date || rcmail.gettext('nodate','tasklist')) + '</span>' +
'<a href="#" class="actions">V</a>'
)
.data('id', rec.id)
.draggable({
revert: 'invalid',
addClasses: false,
cursorAt: { left:-10, top:12 },
helper: draggable_helper,
appendTo: 'body',
start: draggable_start,
stop: draggable_stop,
revertDuration: 300
});
if (rec.complete == 1.0)
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 = $('<li>')
.attr('rel', rec.id)
.addClass('taskitem')
.append((rec.collapsed ? '<span class="childtoggle collapsed">&#9654;' : '<span class="childtoggle expanded">&#9660;') + '</span>')
.append(div)
.append('<ul class="childtasks" style="' + (rec.collapsed ? 'display:none' : '') + '"></ul>');
if (!parent || !parent.length)
li.appendTo(rcmail.gui_objects.resultlist);
}
if (!inplace && parent && parent.length)
li.appendTo(parent);
if (replace) {
resort_task(rec, li, true);
// TODO: remove the item after a while if it doesn't match the current filter anymore
}
}
/**
* Move the given task item to the right place in the list
*/
function resort_task(rec, li, animated)
{
var dir = 0, index, slice, next_li, next_id, next_rec;
// 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);
});
}
// 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.siblings().each(function(i, elem){
next_li = $(elem);
next_id = next_li.attr('rel');
next_rec = listdata[next_id];
if (next_id == rec.id) {
next_li = null;
return 1; // continue
}
if (next_rec && task_cmp(rec, next_rec) > 0) {
return 1; // continue;
}
else if (next_rec && next_li && task_cmp(rec, next_rec) < 0) {
if (animated) insert_animated(li, next_li);
else li.insertBefore(next_li);
next_li = null;
return false;
}
});
index = $.inArray(next_id, listindex);
if (next_li) {
if (animated) insert_animated(li, null, next_li);
else li.insertAfter(next_li);
index++;
}
// 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)
{
var d = Math.floor(a.complete) - Math.floor(b.complete);
if (!d) d = (b._hasdate-0) - (a._hasdate-0);
if (!d) d = (a.datetime||99999999999) - (b.datetime||99999999999);
return d;
}
/**
*
*/
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 draggable_helper()
{
if (!draghelper)
draghelper = $('<div class="taskitem-draghelper">&#x2714;</div>');
return draghelper;
}
function draggable_start(event, ui)
{
$('.taskhead, #rootdroppable, #'+rcmail.gui_objects.folderlist.id+' li').droppable({
hoverClass: 'droptarget',
accept: droppable_accept,
drop: draggable_dropped,
addClasses: false
});
$(this).parent().addClass('dragging');
$('#rootdroppable').show();
}
function draggable_stop(event, ui)
{
$(this).parent().removeClass('dragging');
$('#rootdroppable').hide();
}
function 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 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();
});
}
}
}
/**
* Show task details in a dialog
*/
function task_show_dialog(id)
{
var $dialog = $('#taskshow'), rec;
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
if (!(rec = listdata[id]) || clear_popups({}))
return;
me.selected_task = rec;
// fill dialog data
$('#task-parent-title').html(Q(rec.parent_title || '')+' &raquo;').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')]().children('.task-text').html(Q(rec.date || rcmail.gettext('nodate','tasklist')));
$('#task-time').html(Q(rec.time || ''));
$('#task-start')[(rec.startdate ? 'show' : 'hide')]().children('.task-text').html(Q(rec.startdate || ''));
$('#task-starttime').html(Q(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-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : ''));
var itags = get_inherited_tags(rec);
var taglist = $('#task-tags')[(rec.tags && rec.tags.length || itags.length ? 'show' : 'hide')]().children('.task-text').empty();
if (rec.tags && rec.tags.length) {
$.each(rec.tags, function(i,val){
$('<span>').addClass('tag-element').html(Q(val)).appendTo(taglist);
});
}
// append inherited tags
if (itags.length) {
$.each(itags, function(i,val){
$('<span>').addClass('tag-element inherit').html(Q(val)).appendTo(taglist);
});
// re-sort tags list
$(taglist).children().sortElements(function(a,b){
return $.text([a]).toLowerCase() > $.text([b]).toLowerCase() ? 1 : -1;
});
}
// 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();
}
}
// define dialog buttons
var buttons = [];
buttons.push({
text: rcmail.gettext('edit','tasklist'),
click: function() {
task_edit_dialog(me.selected_task.id, 'edit');
},
disabled: rcmail.busy
});
buttons.push({
text: rcmail.gettext('delete','tasklist'),
click: function() {
if (delete_task(me.selected_task.id))
$dialog.dialog('close');
},
disabled: rcmail.busy
});
// open jquery UI dialog
$dialog.dialog({
modal: false,
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('taskdetails', 'tasklist'),
open: function() {
$dialog.parent().find('.ui-button').first().focus();
},
close: function() {
$dialog.dialog('destroy').appendTo(document.body);
},
buttons: buttons,
minWidth: 500,
width: 580
}).show();
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 580);
}
/**
* Opens the dialog to edit a task
*/
function task_edit_dialog(id, action, presets)
{
$('#taskshow:ui-dialog').dialog('close');
var rec = listdata[id] || presets,
$dialog = $('<div>'),
editform = $('#taskedit'),
list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] :
(me.selected_list ? me.tasklists[me.selected_list] : { editable: action=='new' });
if (rcmail.busy || !list.editable || (action == 'edit' && (!rec || rec.readonly)))
return false;
me.selected_task = $.extend({ alarms:'' }, 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();
// 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 tasklist = $('#taskedit-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){
$('<input>')
.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') }
});
// set alarm(s)
if (rec.alarms || action != 'new') {
var valarms = (typeof rec.alarms == 'string' ? rec.alarms.split(';') : rec.alarms) || [''];
for (var alarm, i=0; i < valarms.length; i++) {
alarm = String(valarms[i]).split(':');
if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY';
$('#taskedit select.edit-alarm-type').val(alarm[1]);
if (alarm[0].match(/@(\d+)/)) {
var ondate = fromunixtime(parseInt(RegExp.$1));
$('#taskedit select.edit-alarm-offset').val('@');
$('#taskedit input.edit-alarm-date').val(me.format_datetime(ondate, 1));
$('#taskedit input.edit-alarm-time').val(me.format_datetime(ondate, 2));
}
else if (alarm[0].match(/([-+])(\d+)([MHD])/)) {
$('#taskedit input.edit-alarm-value').val(RegExp.$2);
$('#taskedit select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3);
}
break; // only one alarm is currently supported
}
}
// set correct visibility by triggering onchange handlers
$('#taskedit select.edit-alarm-type, #taskedit select.edit-alarm-offset').change();
// attachments
rcmail.enable_command('remove-attachment', 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')]();
// activate the first tab
$('#taskedit').tabs('select', 0);
// define dialog buttons
var buttons = {};
buttons[rcmail.gettext('save', 'tasklist')] = function() {
// copy form field contents into task object to save
$.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, list:tasklist }, function(key,input){
me.selected_task[key] = input.val();
});
me.selected_task.tags = [];
me.selected_task.attachments = [];
// do some basic input validation
if (!me.selected_task.title || !me.selected_task.title.length) {
title.focus();
return false;
}
else if (me.selected_task.startdate && me.selected_task.date) {
var startdate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.startdate, datepicker_settings);
var duedate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.date, datepicker_settings);
if (startdate > duedate) {
alert(rcmail.gettext('invalidstartduedates', 'tasklist'));
return false;
}
}
// collect tags
$('input[type="hidden"]', rcmail.gui_objects.edittagline).each(function(i,elem){
if (elem.value)
me.selected_task.tags.push(elem.value);
});
// including the "pending" one in the text box
var newtag = $('#tagedit-input').val();
if (newtag != '') {
me.selected_task.tags.push(newtag);
}
// serialize alarm settings
var alarm = $('#taskedit select.edit-alarm-type').val();
if (alarm) {
var val, offset = $('#taskedit select.edit-alarm-offset').val();
if (offset == '@')
me.selected_task.alarms = '@' + date2unixtime(parse_datetime($('#taskedit input.edit-alarm-time').val(), $('#taskedit input.edit-alarm-date').val())) + ':' + alarm;
else if ((val = parseInt($('#taskedit input.edit-alarm-value').val())) && !isNaN(val) && val >= 0)
me.selected_task.alarms = offset[0] + val + offset[1] + ':' + alarm;
}
// uploaded attachments list
for (var i in rcmail.env.attachments) {
if (i.match(/^rcmfile(.+)/))
me.selected_task.attachments.push(RegExp.$1);
}
// task assigned to a new list
if (me.selected_task.list && me.selected_task.list != rec.list) {
me.selected_task._fromlist = rec.list;
}
me.selected_task.complete = complete.val() / 100;
if (isNaN(me.selected_task.complete))
me.selected_task.complete = null;
if (!me.selected_task.list && list.id)
me.selected_task.list = list.id;
if (save_task(me.selected_task, action))
$dialog.dialog('close');
};
if (action != 'new') {
buttons[rcmail.gettext('delete', 'tasklist')] = function() {
if (delete_task(rec.id))
$dialog.dialog('close');
};
}
buttons[rcmail.gettext('cancel', 'tasklist')] = function() {
$dialog.dialog('close');
};
// open jquery UI dialog
$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() {
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
title.select();
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 580);
}
/**
* Open a task attachment either in a browser window for inline view or download it
*/
function load_attachment(rec, att)
{
// can't open temp attachments
if (!rec.id || rec.id < 0)
return false;
var qstring = '_id='+urlencode(att.id)+'&_t='+urlencode(rec.recurrence_id||rec.id)+'&_list='+urlencode(rec.list);
// open attachment in frame if it's of a supported mimetype
// similar as in app.js and calendar_ui.js
if (att.id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) {
if (rcmail.open_window(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', true, true)) {
return;
}
}
rcmail.goto_url('get-attachment', qstring+'&_download=1', false);
};
/**
* Build task attachments list
*/
function task_show_attachments(list, container, rec, edit)
{
var i, id, len, content, li, elem,
ul = $('<ul>').addClass('attachmentslist');
for (i=0, len=list.length; i<len; i++) {
elem = list[i];
li = $('<li>').addClass(elem.classname);
if (edit) {
rcmail.env.attachments[elem.id] = elem;
// delete icon
content = $('<a>')
.attr('href', '#delete')
.attr('title', rcmail.gettext('delete'))
.addClass('delete')
.click({ id:elem.id }, function(e) {
remove_attachment(this, e.data.id);
return false;
});
if (!rcmail.env.deleteicon) {
content.html(rcmail.gettext('delete'));
}
else {
$('<img>').attr('src', rcmail.env.deleteicon).attr('alt', rcmail.gettext('delete')).appendTo(content);
}
li.append(content);
}
// name/link
$('<a>')
.attr('href', '#load')
.addClass('file')
.html(elem.name).click({ task:rec, att:elem }, function(e) {
load_attachment(e.data.task, e.data.att);
return false;
}).appendTo(li);
ul.append(li);
}
if (edit && rcmail.gui_objects.attachmentlist) {
ul.id = rcmail.gui_objects.attachmentlist.id;
rcmail.gui_objects.attachmentlist = ul.get(0);
}
container.empty().append(ul);
};
/**
*
*/
var remove_attachment = function(elem, id)
{
$(elem.parentNode).hide();
me.selected_task.deleted_attachments.push(id);
delete rcmail.env.attachments[id];
};
/**
*
*/
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 = [{
text: rcmail.gettext('cancel', 'tasklist'),
click: function() {
$(this).dialog('close');
}
}];
if (rec.children && rec.children.length) {
html = rcmail.gettext('deleteparenttasktconfirm','tasklist');
buttons.push({
text: rcmail.gettext('deletethisonly','tasklist'),
click: function() {
_delete_task(id, 0);
$(this).dialog('close');
}
});
buttons.push({
text: rcmail.gettext('deletewithchilds','tasklist'),
click: function() {
_delete_task(id, 1);
$(this).dialog('close');
}
});
}
else {
html = rcmail.gettext('deletetasktconfirm','tasklist');
buttons.push({
text: rcmail.gettext('delete','tasklist'),
click: function() {
_delete_task(id, 0);
$(this).dialog('close');
}
});
}
var $dialog = $('<div>').html(html);
$dialog.dialog({
modal: true,
width: 520,
dialogClass: 'warning',
title: rcmail.gettext('deletetask', 'tasklist'),
buttons: buttons,
close: function(){
$dialog.dialog('destroy').hide();
}
}).addClass('tasklist-confirm').show();
return true;
}
/**
* Subfunction to submit the delete command after confirm
*/
function _delete_task(id, mode)
{
var rec = listdata[id],
li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide();
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list }, 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();
}
/**
* Check if the given task matches the current filtermask and tag selection
*/
function match_filter(rec, cache, recursive)
{
// return cached result
if (typeof cache[rec.id] != 'undefined' && recursive != 2) {
return cache[rec.id];
}
var match = !filtermask || (filtermask & rec.mask) > 0
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 itags;
}
/**
*
*/
function list_edit_dialog(id)
{
var list = me.tasklists[id],
$dialog = $('#tasklistform');
editform = $('#tasklisteditform');
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
if (!list)
list = { name:'', editable:true, showalarms:true };
// fill edit form
var name = $('#taskedit-tasklistame').prop('disabled', list.norename||false).val(list.editname || list.name),
alarms = $('#taskedit-showalarms').prop('checked', list.showalarms).get(0),
parent = $('#taskedit-parentfolder').val(list.parentfolder);
// dialog buttons
var buttons = {};
buttons[rcmail.gettext('save','tasklist')] = function() {
// do some input validation
if (!name.val() || name.val().length < 2) {
alert(rcmail.gettext('invalidlistproperties', 'tasklist'));
name.select();
return;
}
// post data to server
var data = editform.serializeJSON();
if (list.id)
data.id = list.id;
if (alarms)
data.showalarms = alarms.checked ? 1 : 0;
if (parent.length)
data.parentfolder = $('option:selected', parent).val();
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('tasklist', { action:(list.id ? 'edit' : 'new'), l:data });
$dialog.dialog('close');
};
buttons[rcmail.gettext('cancel','tasklist')] = function() {
$dialog.dialog('close');
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: false,
title: rcmail.gettext((list.id ? 'editlist' : 'createlist'), 'tasklist'),
close: function() { $dialog.dialog('destroy').hide(); },
buttons: buttons,
minWidth: 400,
width: 420
}).show();
}
/**
*
*/
function list_remove(id)
{
var list = me.tasklists[id];
if (list && !list.norename && confirm(rcmail.gettext(list.children ? 'deletelistconfirmrecursive' : 'deletelistconfirm', 'tasklist'))) {
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('tasklist', { action:'remove', l:{ id:list.id } });
return true;
}
return false;
}
/**
* Callback from server to finally remove the given list
*/
function destroy_list(prop)
{
var li, delete_ids = [],
list = me.tasklists[prop.id];
// find sub-lists
if (list && list.children) {
for (var child_id in me.tasklists) {
if (String(child_id).indexOf(prop.id) == 0)
delete_ids.push(child_id);
}
}
else {
delete_ids.push(prop.id);
}
// delete all calendars in the list
for (var i=0; i < delete_ids.length; i++) {
id = delete_ids[i];
list = me.tasklists[id];
li = rcmail.get_folder_li(id, 'rcmlitasklist');
if (li) {
$(li).remove();
}
if (list) {
list.active = false;
// delete me.tasklists[prop.id];
unlock_saving();
remove_tasks(list.id);
}
}
}
/**
*
*/
function insert_list(prop)
{
var li = $('<li>').attr('id', 'rcmlitasklist'+prop.id)
.append('<input type="checkbox" name="_list[]" value="'+prop.id+'" checked="checked" />')
.append('<span class="handle">&nbsp;</span>')
.append('<span class="listname">'+Q(prop.name)+'</span>');
$(rcmail.gui_objects.folderlist).append(li);
me.tasklists[prop.id] = prop;
init_tasklist_li(li.get(0), prop.id);
}
/**
*
*/
function update_list(prop)
{
var id = prop.oldid || prop.id,
li = rcmail.get_folder_li(id, 'rcmlitasklist');
if (me.tasklists[id] && li) {
delete me.tasklists[id];
me.tasklists[prop.id] = prop;
$(li).data('id', prop.id);
$('#'+li.id+' input').data('id', prop.id);
$('.listname', li).html(Q(prop.name));
}
}
/**
* Execute search
*/
function quicksearch()
{
var q;
if (rcmail.gui_objects.qsearchbox && (q = rcmail.gui_objects.qsearchbox.value)) {
var id = 'search-'+q;
var resources = [];
for (var rid in me.tasklists) {
if (me.tasklists[rid].active) {
resources.push(rid);
}
}
id += '@'+resources.join(',');
// ignore if query didn't change
if (search_request == id)
return;
search_request = id;
search_query = q;
list_tasks();
}
else // empty search input equals reset
this.reset_search();
}
/**
* Reset search and get back to normal listing
*/
function reset_search()
{
$(rcmail.gui_objects.qsearchbox).val('');
if (search_request) {
search_request = search_query = null;
list_tasks();
}
}
/**** Utility functions ****/
/**
* Clear any text selection
* (text is probably selected when double-clicking somewhere)
*/
function clearSelection()
{
if (document.selection && document.selection.empty) {
document.selection.empty() ;
}
else if (window.getSelection) {
var sel = window.getSelection();
if (sel && sel.removeAllRanges)
sel.removeAllRanges();
}
}
/**
* Hide all open popup menus
*/
function clear_popups(e)
{
var count = 0, target = e.target;
if (target && target.className == 'inner')
target = e.target.parentNode;
$('.popupmenu:visible').each(function(i, elem){
var menu = $(elem), id = elem.id;
if (target.id != id+'link' && (!menu.data('sticky') || !target_overlaps(e.target, elem))) {
menu.hide();
count++;
}
});
return count;
}
/**
* Check whether the event target is a descentand of the given element
*/
function target_overlaps(target, elem)
{
while (target.parentNode) {
if (target.parentNode == elem)
return true;
target = target.parentNode;
}
return false;
}
/**
*
*/
function active_lists()
{
var active = [];
for (var id in me.tasklists) {
if (me.tasklists[id].active)
active.push(id);
}
return active;
}
// resize and reposition (center) the dialog window
this.dialog_resize = function(id, height, width)
{
var win = $(window), w = win.width(), h = win.height();
$(id).dialog('option', { height: Math.min(h-20, height+130), width: Math.min(w-20, width+50) })
.dialog('option', 'position', ['center', 'center']); // only works in a separate call (!?)
};
/**
* Register event handlers on a tasklist (folder) item
*/
function init_tasklist_li(li, id)
{
$('#'+li.id+' input').click(function(e){
var id = $(this).data('id');
if (me.tasklists[id]) { // add or remove event source on click
me.tasklists[id].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:me.tasklists[id].active?1:0 } });
}
}).data('id', id).get(0).checked = me.tasklists[id].active || false;
$(li).click(function(e){
var id = $(this).data('id');
rcmail.select_folder(id, 'rcmlitasklist');
rcmail.enable_command('list-edit', 'list-remove', 'list-import', me.tasklists[id].editable);
me.selected_list = id;
})
.dblclick(function(e){
list_edit_dialog($(this).data('id'));
})
.data('id', id)
.data('type', 'tasklist')
.addClass(me.tasklists[id].editable ? null : 'readonly');
}
// init dialog by default
init_taskedit();
}
// extend jQuery
// from http://james.padolsey.com/javascript/sorting-elements-with-jquery/
jQuery.fn.sortElements = (function(){
var sort = [].sort;
return function(comparator, getSortable) {
getSortable = getSortable || function(){ return this };
var last = null;
return sort.call(this, comparator).each(function(i){
// at this point the array is sorted, so we can just detach each one from wherever it is, and add it after the last
var node = $(getSortable.call(this));
var parent = node.parent();
if (last) last.after(node);
else parent.prepend(node);
last = node;
});
};
})();
/* tasklist plugin UI initialization */
var rctasks;
window.rcmail && rcmail.addEventListener('init', function(evt) {
rctasks = new rcube_tasklist_ui(rcmail.env.libcal_settings);
// register button commands
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);
rcmail.register_command('list-edit', function(){ rctasks.list_edit_dialog(rctasks.selected_list); }, false);
rcmail.register_command('list-remove', function(){ rctasks.list_remove(rctasks.selected_list); }, false);
rcmail.register_command('search', function(){ rctasks.quicksearch(); }, true);
rcmail.register_command('reset-search', function(){ rctasks.reset_search(); }, true);
+ rcmail.register_command('expand-all', function(){ rctasks.expand_collapse(true); }, true);
+ rcmail.register_command('collapse-all', function(){ rctasks.expand_collapse(false); }, true);
rctasks.init();
});
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index e6b5d1cd..68d82c4f 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1,917 +1,919 @@
<?php
/**
* Tasks plugin for Roundcube webmail
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
class tasklist extends rcube_plugin
{
const FILTER_MASK_TODAY = 1;
const FILTER_MASK_TOMORROW = 2;
const FILTER_MASK_WEEK = 4;
const FILTER_MASK_LATER = 8;
const FILTER_MASK_NODATE = 16;
const FILTER_MASK_OVERDUE = 32;
const FILTER_MASK_FLAGGED = 64;
const FILTER_MASK_COMPLETE = 128;
const SESSION_KEY = 'tasklist_temp';
public static $filter_masks = array(
'today' => self::FILTER_MASK_TODAY,
'tomorrow' => self::FILTER_MASK_TOMORROW,
'week' => self::FILTER_MASK_WEEK,
'later' => self::FILTER_MASK_LATER,
'nodate' => self::FILTER_MASK_NODATE,
'overdue' => self::FILTER_MASK_OVERDUE,
'flagged' => self::FILTER_MASK_FLAGGED,
'complete' => self::FILTER_MASK_COMPLETE,
);
public $task = '?(?!login|logout).*';
public $rc;
public $lib;
public $driver;
public $timezone;
public $ui;
private $collapsed_tasks = array();
/**
* Plugin initialization.
*/
function init()
{
$this->require_plugin('libcalendaring');
$this->rc = rcube::get_instance();
$this->lib = libcalendaring::get_instance();
$this->register_task('tasks', 'tasklist');
// load plugin configuration
$this->load_config();
// load localizations
$this->add_texts('localization/', $this->rc->task == 'tasks' && (!$this->rc->action || $this->rc->action == 'print'));
$this->rc->load_language($_SESSION['language'], array('tasks.tasks' => $this->gettext('navtitle'))); // add label for task title
$this->timezone = $this->lib->timezone;
if ($this->rc->task == 'tasks' && $this->rc->action != 'save-pref') {
$this->load_driver();
// register calendar actions
$this->register_action('index', array($this, 'tasklist_view'));
$this->register_action('task', array($this, 'task_action'));
$this->register_action('tasklist', array($this, 'tasklist_action'));
$this->register_action('counts', array($this, 'fetch_counts'));
$this->register_action('fetch', array($this, 'fetch_tasks'));
$this->register_action('inlineui', array($this, 'get_inline_ui'));
$this->register_action('mail2task', array($this, 'mail_message2task'));
$this->register_action('get-attachment', array($this, 'attachment_get'));
$this->register_action('upload', array($this, 'attachment_upload'));
$this->add_hook('refresh', array($this, 'refresh'));
$this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
}
else if ($this->rc->task == 'mail') {
// TODO: register hooks to catch ical/vtodo email attachments
if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
// $this->add_hook('message_load', array($this, 'mail_message_load'));
// $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
}
// add 'Create event' item to message menu
if ($this->api->output->type == 'html') {
$this->api->add_content(html::tag('li', null,
$this->api->output->button(array(
'command' => 'tasklist-create-from-mail',
'label' => 'tasklist.createfrommail',
'type' => 'link',
'classact' => 'icon taskaddlink active',
'class' => 'icon taskaddlink',
'innerclass' => 'icon taskadd',
))),
'messagemenu');
}
}
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
require_once($this->home . '/tasklist_ui.php');
$this->ui = new tasklist_ui($this);
$this->ui->init();
}
// add hooks for alarms handling
$this->add_hook('pending_alarms', array($this, 'pending_alarms'));
$this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
}
/**
* Helper method to load the backend driver according to local config
*/
private function load_driver()
{
if (is_object($this->driver))
return;
$driver_name = $this->rc->config->get('tasklist_driver', 'database');
$driver_class = 'tasklist_' . $driver_name . '_driver';
require_once($this->home . '/drivers/tasklist_driver.php');
require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
switch ($driver_name) {
case "kolab":
$this->require_plugin('libkolab');
default:
$this->driver = new $driver_class($this);
break;
}
$this->rc->output->set_env('tasklist_driver', $driver_name);
}
/**
* Dispatcher for task-related actions initiated by the client
*/
public function task_action()
{
$filter = intval(get_input_value('filter', RCUBE_INPUT_GPC));
$action = get_input_value('action', RCUBE_INPUT_GPC);
$rec = get_input_value('t', RCUBE_INPUT_POST, true);
$oldrec = $rec;
$success = $refresh = false;
switch ($action) {
case 'new':
$oldrec = null;
$rec = $this->prepare_task($rec);
$rec['uid'] = $this->generate_uid();
$temp_id = $rec['tempid'];
if ($success = $this->driver->create_task($rec)) {
$refresh = $this->driver->get_task($rec);
if ($temp_id) $refresh['tempid'] = $temp_id;
$this->cleanup_task($rec);
}
break;
case 'edit':
$rec = $this->prepare_task($rec);
if ($success = $this->driver->edit_task($rec)) {
$refresh = $this->driver->get_task($rec);
$this->cleanup_task($rec);
}
break;
case 'move':
foreach ((array)$rec['id'] as $id) {
$r = $rec;
$r['id'] = $id;
if ($this->driver->move_task($r)) {
$refresh[] = $this->driver->get_task($r);
$success = true;
// move all childs, too
foreach ($this->driver->get_childs(array('id' => $rec['id'], 'list' => $rec['_fromlist'])) as $cid) {
$child = $rec;
$child['id'] = $cid;
if ($this->driver->move_task($child)) {
$r = $this->driver->get_task($child);
if ((bool)($filter & self::FILTER_MASK_COMPLETE) == ($r['complete'] == 1.0)) {
$refresh[] = $r;
}
}
}
}
}
break;
case 'delete':
$mode = intval(get_input_value('mode', RCUBE_INPUT_POST));
$oldrec = $this->driver->get_task($rec);
if ($success = $this->driver->delete_task($rec, false)) {
// delete/modify all childs
foreach ($this->driver->get_childs($rec, $mode) as $cid) {
$child = array('id' => $cid, 'list' => $rec['list']);
if ($mode == 1) { // delete all childs
if ($this->driver->delete_task($child, false)) {
if ($this->driver->undelete)
$_SESSION['tasklist_undelete'][$rec['id']][] = $cid;
}
else
$success = false;
}
else {
$child['parent_id'] = strval($oldrec['parent_id']);
$this->driver->edit_task($child);
}
}
}
if (!$success)
$this->rc->output->command('plugin.reload_data');
break;
case 'undelete':
if ($success = $this->driver->undelete_task($rec)) {
$refresh[] = $this->driver->get_task($rec);
foreach ((array)$_SESSION['tasklist_undelete'][$rec['id']] as $cid) {
if ($this->driver->undelete_task($rec)) {
$refresh[] = $this->driver->get_task($rec);
}
}
}
break;
case 'collapse':
- if (intval(get_input_value('collapsed', RCUBE_INPUT_GPC))) {
- $this->collapsed_tasks[] = $rec['id'];
- }
- else {
- $i = array_search($rec['id'], $this->collapsed_tasks);
- if ($i !== false)
- unset($this->collapsed_tasks[$i]);
+ foreach (explode(',', $rec['id']) as $rec_id) {
+ if (intval(get_input_value('collapsed', RCUBE_INPUT_GPC))) {
+ $this->collapsed_tasks[] = $rec_id;
+ }
+ else {
+ $i = array_search($rec_id, $this->collapsed_tasks);
+ if ($i !== false)
+ unset($this->collapsed_tasks[$i]);
+ }
}
$this->rc->user->save_prefs(array('tasklist_collapsed_tasks' => join(',', array_unique($this->collapsed_tasks))));
return; // avoid further actions
}
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
$this->update_counts($oldrec, $refresh);
}
else
$this->rc->output->show_message('tasklist.errorsaving', 'error');
// unlock client
$this->rc->output->command('plugin.unlock_saving');
if ($refresh) {
if ($refresh['id']) {
$this->encode_task($refresh);
}
else if (is_array($refresh)) {
foreach ($refresh as $i => $r)
$this->encode_task($refresh[$i]);
}
$this->rc->output->command('plugin.refresh_task', $refresh);
}
}
/**
* repares new/edited task properties before save
*/
private function prepare_task($rec)
{
// try to be smart and extract date from raw input
if ($rec['raw']) {
foreach (array('today','tomorrow','sunday','monday','tuesday','wednesday','thursday','friday','saturday','sun','mon','tue','wed','thu','fri','sat') as $word) {
$locwords[] = '/^' . preg_quote(mb_strtolower($this->gettext($word))) . '\b/i';
$normwords[] = $word;
$datewords[] = $word;
}
foreach (array('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','now','dec') as $month) {
$locwords[] = '/(' . preg_quote(mb_strtolower($this->gettext('long'.$month))) . '|' . preg_quote(mb_strtolower($this->gettext($month))) . ')\b/i';
$normwords[] = $month;
$datewords[] = $month;
}
foreach (array('on','this','next','at') as $word) {
$fillwords[] = preg_quote(mb_strtolower($this->gettext($word)));
$fillwords[] = $word;
}
$raw = trim($rec['raw']);
$date_str = '';
// translate localized keywords
$raw = preg_replace('/^(' . join('|', $fillwords) . ')\s*/i', '', $raw);
$raw = preg_replace($locwords, $normwords, $raw);
// find date pattern
$date_pattern = '!^(\d+[./-]\s*)?((?:\d+[./-])|' . join('|', $datewords) . ')\.?(\s+\d{4})?[:;,]?\s+!i';
if (preg_match($date_pattern, $raw, $m)) {
$date_str .= $m[1] . $m[2] . $m[3];
$raw = preg_replace(array($date_pattern, '/^(' . join('|', $fillwords) . ')\s*/i'), '', $raw);
// add year to date string
if ($m[1] && !$m[3])
$date_str .= date('Y');
}
// find time pattern
$time_pattern = '/^(\d+([:.]\d+)?(\s*[hapm.]+)?),?\s+/i';
if (preg_match($time_pattern, $raw, $m)) {
$has_time = true;
$date_str .= ($date_str ? ' ' : 'today ') . $m[1];
$raw = preg_replace($time_pattern, '', $raw);
}
// yes, raw input matched a (valid) date
if (strlen($date_str) && strtotime($date_str) && ($date = new DateTime($date_str, $this->timezone))) {
$rec['date'] = $date->format('Y-m-d');
if ($has_time)
$rec['time'] = $date->format('H:i');
$rec['title'] = $raw;
}
else
$rec['title'] = $rec['raw'];
}
// normalize input from client
if (isset($rec['complete'])) {
$rec['complete'] = floatval($rec['complete']);
if ($rec['complete'] > 1)
$rec['complete'] /= 100;
}
if (isset($rec['flagged']))
$rec['flagged'] = intval($rec['flagged']);
// fix for garbage input
if ($rec['description'] == 'null')
$rec['description'] = '';
foreach ($rec as $key => $val) {
if ($val === 'null')
$rec[$key] = null;
}
if (!empty($rec['date'])) {
try {
$date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
$rec['date'] = $date->format('Y-m-d');
if (!empty($rec['time']))
$rec['time'] = $date->format('H:i');
}
catch (Exception $e) {
$rec['date'] = $rec['time'] = null;
}
}
if (!empty($rec['startdate'])) {
try {
$date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
$rec['startdate'] = $date->format('Y-m-d');
if (!empty($rec['starttime']))
$rec['starttime'] = $date->format('H:i');
}
catch (Exception $e) {
$rec['startdate'] = $rec['starttime'] = null;
}
}
// alarms cannot work without a date
if ($rec['alarms'] && !$rec['date'] && !$rec['startdate'] && strpos($rec['alarms'], '@') === false)
$rec['alarms'] = '';
$attachments = array();
$taskid = $rec['id'];
if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) {
if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
if (is_array($rec['attachments']) && in_array($id, $rec['attachments'])) {
$attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
unset($attachments[$id]['abort'], $attachments[$id]['group']);
}
}
}
}
$rec['attachments'] = $attachments;
if (is_numeric($rec['id']) && $rec['id'] < 0)
unset($rec['id']);
return $rec;
}
/**
* Releases some resources after successful save
*/
private function cleanup_task(&$rec)
{
// remove temp. attachment files
if (!empty($_SESSION[self::SESSION_KEY]) && ($taskid = $_SESSION[self::SESSION_KEY]['id'])) {
$this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $taskid));
$this->rc->session->remove(self::SESSION_KEY);
}
}
/**
* Dispatcher for tasklist actions initiated by the client
*/
public function tasklist_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$list = get_input_value('l', RCUBE_INPUT_POST, true);
$success = false;
if (isset($list['showalarms']))
$list['showalarms'] = intval($list['showalarms']);
switch ($action) {
case 'new':
$list += array('showalarms' => true, 'active' => true, 'editable' => true);
if ($insert_id = $this->driver->create_list($list)) {
$list['id'] = $insert_id;
$this->rc->output->command('plugin.insert_tasklist', $list);
$success = true;
}
break;
case 'edit':
if ($newid = $this->driver->edit_list($list)) {
$list['oldid'] = $list['id'];
$list['id'] = $newid;
$this->rc->output->command('plugin.update_tasklist', $list);
$success = true;
}
break;
case 'subscribe':
$success = $this->driver->subscribe_list($list);
break;
case 'remove':
if (($success = $this->driver->remove_list($list)))
$this->rc->output->command('plugin.destroy_tasklist', $list);
break;
}
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('tasklist.errorsaving', 'error');
$this->rc->output->command('plugin.unlock_saving');
}
/**
* Get counts for active tasks divided into different selectors
*/
public function fetch_counts()
{
if (isset($_REQUEST['lists'])) {
$lists = get_input_value('lists', RCUBE_INPUT_GPC);
}
else {
foreach ($this->driver->get_lists() as $list) {
if ($list['active'])
$lists[] = $list['id'];
}
}
$counts = $this->driver->count_tasks($lists);
$this->rc->output->command('plugin.update_counts', $counts);
}
/**
* Adjust the cached counts after changing a task
*/
public function update_counts($oldrec, $newrec)
{
// rebuild counts until this function is finally implemented
$this->fetch_counts();
// $this->rc->output->command('plugin.update_counts', $counts);
}
/**
*
*/
public function fetch_tasks()
{
$f = intval(get_input_value('filter', RCUBE_INPUT_GPC));
$search = get_input_value('q', RCUBE_INPUT_GPC);
$filter = array('mask' => $f, 'search' => $search);
$lists = get_input_value('lists', RCUBE_INPUT_GPC);;
/*
// convert magic date filters into a real date range
switch ($f) {
case self::FILTER_MASK_TODAY:
$today = new DateTime('now', $this->timezone);
$filter['from'] = $filter['to'] = $today->format('Y-m-d');
break;
case self::FILTER_MASK_TOMORROW:
$tomorrow = new DateTime('now + 1 day', $this->timezone);
$filter['from'] = $filter['to'] = $tomorrow->format('Y-m-d');
break;
case self::FILTER_MASK_OVERDUE:
$yesterday = new DateTime('yesterday', $this->timezone);
$filter['to'] = $yesterday->format('Y-m-d');
break;
case self::FILTER_MASK_WEEK:
$today = new DateTime('now', $this->timezone);
$filter['from'] = $today->format('Y-m-d');
$weekend = new DateTime('now + 7 days', $this->timezone);
$filter['to'] = $weekend->format('Y-m-d');
break;
case self::FILTER_MASK_LATER:
$date = new DateTime('now + 8 days', $this->timezone);
$filter['from'] = $date->format('Y-m-d');
break;
}
*/
$data = $this->tasks_data($this->driver->list_tasks($filter, $lists), $f, $tags);
$this->rc->output->command('plugin.data_ready', array('filter' => $f, 'lists' => $lists, 'search' => $search, 'data' => $data, 'tags' => array_values(array_unique($tags))));
}
/**
* Prepare and sort the given task records to be sent to the client
*/
private function tasks_data($records, $f, &$tags)
{
$data = $tags = $this->task_tree = $this->task_titles = array();
foreach ($records as $rec) {
if ($rec['parent_id']) {
$this->task_tree[$rec['id']] = $rec['parent_id'];
}
$this->encode_task($rec);
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))
$data[] = $rec;
}
// sort tasks according to their hierarchy level and due date
array_walk($data, array($this, 'task_walk_tree'));
usort($data, array($this, 'task_sort_cmp'));
return $data;
}
/**
* Prepare the given task record before sending it to the client
*/
private function encode_task(&$rec)
{
$rec['mask'] = $this->filter_mask($rec);
$rec['flagged'] = intval($rec['flagged']);
$rec['complete'] = floatval($rec['complete']);
$rec['changed'] = is_object($rec['changed']) ? $rec['changed']->format('U') : null;
if ($rec['date']) {
try {
$date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
$rec['datetime'] = intval($date->format('U'));
$rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
$rec['_hasdate'] = 1;
}
catch (Exception $e) {
$rec['date'] = $rec['datetime'] = null;
}
}
else {
$rec['date'] = $rec['datetime'] = null;
$rec['_hasdate'] = 0;
}
if ($rec['startdate']) {
try {
$date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
$rec['startdatetime'] = intval($date->format('U'));
$rec['startdate'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
}
catch (Exception $e) {
$rec['startdate'] = $rec['startdatetime'] = null;
}
}
if ($rec['alarms'])
$rec['alarms_text'] = libcalendaring::alarms_text($rec['alarms']);
foreach ((array)$rec['attachments'] as $k => $attachment) {
$rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
}
if (!is_array($rec['tags']))
$rec['tags'] = (array)$rec['tags'];
sort($rec['tags'], SORT_LOCALE_STRING);
if (in_array($rec['id'], $this->collapsed_tasks))
$rec['collapsed'] = true;
$this->task_titles[$rec['id']] = $rec['title'];
}
/**
* Callback function for array_walk over all tasks.
* Sets tree depth and parent titles
*/
private function task_walk_tree(&$rec)
{
$rec['_depth'] = 0;
$parent_id = $this->task_tree[$rec['id']];
while ($parent_id) {
$rec['_depth']++;
$rec['parent_title'] = $this->task_titles[$parent_id];
$parent_id = $this->task_tree[$parent_id];
}
}
/**
* Compare function for task list sorting.
* Nested tasks need to be sorted to the end.
*/
private function task_sort_cmp($a, $b)
{
$d = $a['_depth'] - $b['_depth'];
if (!$d) $d = $b['_hasdate'] - $a['_hasdate'];
if (!$d) $d = $a['datetime'] - $b['datetime'];
return $d;
}
/**
* Compute the filter mask of the given task
*
* @param array Hash array with Task record properties
* @return int Filter mask
*/
public function filter_mask($rec)
{
static $today, $tomorrow, $weeklimit;
if (!$today) {
$today_date = new DateTime('now', $this->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
$week_date = new DateTime('now + 7 days', $this->timezone);
$weeklimit = $week_date->format('Y-m-d');
}
$mask = 0;
$start = $rec['startdate'] ?: '1900-00-00';
$duedate = $rec['date'] ?: '3000-00-00';
if ($rec['flagged'])
$mask |= self::FILTER_MASK_FLAGGED;
if ($rec['complete'] == 1.0)
$mask |= self::FILTER_MASK_COMPLETE;
if (empty($rec['date']))
$mask |= self::FILTER_MASK_NODATE;
else if ($rec['date'] < $today)
$mask |= self::FILTER_MASK_OVERDUE;
if ($duedate <= $today || ($rec['startdate'] && $start <= $today))
$mask |= self::FILTER_MASK_TODAY;
if ($duedate <= $tomorrow || ($rec['startdate'] && $start <= $tomorrow))
$mask |= self::FILTER_MASK_TOMORROW;
if ($start > $tomorrow || ($duedate > $tomorrow && $duedate <= $weeklimit))
$mask |= self::FILTER_MASK_WEEK;
if ($start > $weeklimit || ($rec['date'] && $duedate > $weeklimit))
$mask |= self::FILTER_MASK_LATER;
return $mask;
}
/******* UI functions ********/
/**
* Render main view of the tasklist task
*/
public function tasklist_view()
{
$this->ui->init();
$this->ui->init_templates();
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('tasklist.mainview');
}
/**
*
*/
public function get_inline_ui()
{
foreach (array('save','cancel','savingdata') as $label)
$texts['tasklist.'.$label] = $this->gettext($label);
$texts['tasklist.newtask'] = $this->gettext('createfrommail');
$this->ui->init_templates();
echo $this->api->output->parse('tasklist.taskedit', false, false);
echo html::tag('script', array('type' => 'text/javascript'),
"rcmail.set_env('tasklists', " . json_encode($this->api->output->env['tasklists']) . ");\n".
// "rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n".
// "rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n".
// "rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n".
"rcmail.add_label(" . json_encode($texts) . ");\n"
);
exit;
}
/**
* Handler for keep-alive requests
* This will check for updated data in active lists and sync them to the client
*/
public function refresh($attr)
{
$filter = array(
'since' => $attr['last'],
'search' => get_input_value('q', RCUBE_INPUT_GPC),
'mask' => intval(get_input_value('filter', RCUBE_INPUT_GPC)) & self::FILTER_MASK_COMPLETE,
);
$lists = get_input_value('lists', RCUBE_INPUT_GPC);;
$updates = $this->driver->list_tasks($filter, $lists);
if (!empty($updates)) {
$this->rc->output->command('plugin.refresh_task', $this->tasks_data($updates, 255, $tags));
// update counts
$counts = $this->driver->count_tasks($lists);
$this->rc->output->command('plugin.update_counts', $counts);
}
}
/**
* Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
* This will check for pending notifications and pass them to the client
*/
public function pending_alarms($p)
{
$this->load_driver();
if ($alarms = $this->driver->pending_alarms($p['time'] ?: time())) {
foreach ($alarms as $alarm) {
// encode alarm object to suit the expectations of the calendaring code
if ($alarm['date'])
$alarm['start'] = new DateTime($alarm['date'].' '.$alarm['time'], $this->timezone);
$alarm['id'] = 'task:' . $alarm['id']; // prefix ID with task:
$alarm['allday'] = empty($alarm['time']) ? 1 : 0;
$p['alarms'][] = $alarm;
}
}
return $p;
}
/**
* Handler for alarm dismiss hook triggered by the calendar module
*/
public function dismiss_alarms($p)
{
$this->load_driver();
foreach ((array)$p['ids'] as $id) {
if (strpos($id, 'task:') === 0)
$p['success'] |= $this->driver->dismiss_alarm(substr($id, 5), $p['snooze']);
}
return $p;
}
/******* Attachment handling *******/
/**
* Handler for attachments upload
*/
public function attachment_upload()
{
$this->lib->attachment_upload(self::SESSION_KEY);
}
/**
* Handler for attachments download/displaying
*/
public function attachment_get()
{
// show loading page
if (!empty($_GET['_preload'])) {
return $this->lib->attachment_loading_page();
}
$task = get_input_value('_t', RCUBE_INPUT_GPC);
$list = get_input_value('_list', RCUBE_INPUT_GPC);
$id = get_input_value('_id', RCUBE_INPUT_GPC);
$task = array('id' => $task, 'list' => $list);
$attachment = $this->driver->get_attachment($id, $task);
// show part page
if (!empty($_GET['_frame'])) {
$this->lib->attachment = $attachment;
$this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame'));
$this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header'));
$this->rc->output->send('tasklist.attachment');
}
// deliver attachment content
else if ($attachment) {
$attachment['body'] = $this->driver->get_attachment_body($id, $task);
$this->lib->attachment_get($attachment);
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/******* Email related function *******/
public function mail_message2task()
{
$uid = get_input_value('_uid', RCUBE_INPUT_POST);
$mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
$task = array();
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_mailbox($mbox);
$message = new rcube_message($uid);
if ($message->headers) {
$task['title'] = trim($message->subject);
$task['description'] = trim($message->first_text_part());
$task['id'] = -$uid;
$this->load_driver();
// copy mail attachments to task
if ($message->attachments && $this->driver->attachments) {
if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $task['id']) {
$_SESSION[self::SESSION_KEY] = array();
$_SESSION[self::SESSION_KEY]['id'] = $task['id'];
$_SESSION[self::SESSION_KEY]['attachments'] = array();
}
foreach ((array)$message->attachments as $part) {
$attachment = array(
'data' => $imap->get_message_part($uid, $part->mime_id, $part),
'size' => $part->size,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'group' => $task['id'],
);
$attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
$attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
// store new attachment in session
unset($attachment['status'], $attachment['abort'], $attachment['data']);
$_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment;
$attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new'
$task['attachments'][] = $attachment;
}
}
}
$this->rc->output->command('plugin.mail2taskdialog', $task);
}
else {
$this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
}
$this->rc->output->send();
}
/******* Utility functions *******/
/**
* Generate a unique identifier for an event
*/
public function generate_uid()
{
return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Apr 24, 2:29 PM (1 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18873376
Default Alt Text
(121 KB)

Event Timeline