diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css index 4a035cb0..c8f36b14 100644 --- a/plugins/calendar/skins/larry/calendar.css +++ b/plugins/calendar/skins/larry/calendar.css @@ -1,2373 +1,2378 @@ /** * Roundcube Calendar plugin styles for skin "Larry" * * Copyright (c) 2012-2014, Kolab Systems AG * 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. */ body.calendarmain { overflow: hidden; } body.calendarmain #mainscreen { left: 0; } /* overrides for tablets and mobile phones */ @media screen and (max-device-width: 1024px){ body.calendarmain { overflow: visible; } body.calendarmain #mainscreen { min-width: 1000px !important; min-height: 520px !important; } body.calendarmain #header { min-width: 1020px !important; } } #calendarsidebar { position: absolute; top: 0; left: 10px; bottom: 0; width: 250px; } #datepicker { position: absolute; top: 40px; left: 0; width: 100%; min-height: 190px; } #datepicker .ui-datepicker { width: 100% !important; box-shadow: none; -moz-box-shadow: none; -webkit-box-shadow: none; } #datepicker .ui-datepicker td a { padding: 5px 4px; font-size: 12px; } #datepicker td.ui-datepicker-activerange { border-color: #69a2b6; } #datepicker .ui-datepicker-activerange a { color: #185d7a; background: #d9f1fb; background: -moz-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d9f1fb), color-stop(100%,#c5e3ee)); background: -o-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%); background: -ms-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%); background: linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d9f1fb', endColorstr='#c5e3ee', GradientType=0); } #datepicker .ui-datepicker-days-cell-over a.ui-state-default { color: #fff; border-color: #2fa0c0; background: rgba(73,180,210,0.6); text-shadow: 0px 1px 1px #666; filter: none; } #datepicker .ui-datepicker-activerange a.ui-state-active { color: #fff; background: #00acd4; background: -moz-linear-gradient(top, #00acd4 0%, #008fc7 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00acd4), color-stop(100%,#008fc7)); background: -o-linear-gradient(top, #00acd4 0%, #008fc7 100%); background: -ms-linear-gradient(top, #00acd4 0%, #008fc7 100%); background: linear-gradient(top, #00acd4 0%, #008fc7 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00acd4', endColorstr='#008fc7', GradientType=0); } #datepicker td.ui-datepicker-week-col { cursor: pointer; } #datepicker .ui-datepicker-title { margin: 2px 2.3em 3px 2.3em; } #datepicker .ui-datepicker .ui-datepicker-prev, #datepicker .ui-datepicker .ui-datepicker-next { top: 4px; } #calsidebarsplitter { position: absolute; left: 264px; width: 6px; top: 40px !important; bottom: 0; height: auto; background: url(images/toggle.gif) -1px 48% no-repeat transparent; } div.sidebarclosed { background-position: -8px 48% !important; cursor: pointer; } #calsidebarsplitter:hover { background-color: #ddd; } #calendar { position: absolute; top: 0; left: 276px; right: 0; bottom: 0; } .calendarmain #message.statusbar { border: 1px solid #c3c3c3; border-bottom-color: #ababab; } #timezonedisplay { position: absolute; bottom: 5px; right: 12px; font-size: 0.85em; color: #666; } #print { width: 680px; } pre { font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif; } #calendars { position: absolute; top: 276px; left: 0; bottom: 0; right: 0; } #calendars .boxtitle { position: relative; } #calendars .boxtitle a.iconbutton.search { position: absolute; top: 8px; right: 8px; width: 16px; cursor: pointer; background-position: -2px -317px; } #calendars .listsearchbox { display: none; } #calendars .listsearchbox.expanded { display: block; } #calendars .scroller { top: 34px; } #calendars .listsearchbox.expanded + .scroller { top: 68px; } #calendars .treelist li { margin: 0; position: relative; } #calendars .treelist li div.folder, #calendars .treelist li div.calendar { position: relative; height: 28px; } #calendars .treelist li div.virtual { height: 22px; } #calendars .treelist li span.calname { display: block; padding: 0px 18px 2px 2px; position: absolute; top: 7px; left: 38px; right: 45px; cursor: default; background: url(images/calendars.png) right 20px no-repeat; overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap; color: #004458; } .quickview-active #calendars .treelist div input, .quickview-active #calendars .treelist div .calname { opacity: 0.35; } .quickview-active #calendars .treelist div.focusview .calname { opacity: 1.0; background-image: none; } #calendars .treelist li div.virtual > span.calname { color: #aaa; top: 4px; left: 20px; } #calendars .treelist li.x-birthdays span.calname, #calendars .treelist li.x-invitations span.calname { font-style: italic; } #calendars .treelist.flat li span.calname { left: 24px; right: 42px; } #calendars .treelist li span.handle { display: inline-block; position: absolute; top: 8px; right: 6px; padding: 0; width: 10px; height: 10px; border-radius: 7px; font-size: 0.8em; border: 1px solid rgba(0, 0, 0, 0.5); -webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3); -moz-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3); box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3); } #calendars .treelist div span.actions { display: inline-block; position: absolute; top: 2px; right: 22px; padding: 5px 20px 0 6px; /* min-width: 40px; */ height: 19px; text-align: right; z-index: 4; } #calendars .treelist div:hover span.actions { top: 1px; right: 21px; border: 1px solid #c6c6c6; border-radius: 4px; background: #f7f7f7; background: -moz-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e6e6e6)); background: -o-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%); background: -ms-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%); background: linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f9f9f9', endColorstr='#e6e6e6', GradientType=0); } #calendars .treelist li a.subscribed { display: inline-block; position: absolute; top: 5px; right: 3px; height: 16px; width: 16px; padding: 0; background: url(images/calendars.png) -100px 0 no-repeat; overflow: hidden; text-indent: -5000px; cursor: pointer; } #calendars .treelist div:hover a.subscribed, #calendars .treelist div a.subscribed:focus { background-position: 0 -110px; } #calendars .treelist div.subscribed a.subscribed, #calendars .treelist div.subscribed a.subscribed:focus { background-position: -16px -110px; } #calendars .treelist div.subscribed.partial a.subscribed, #calendars .treelist div.subscribed.partial a.subscribed:focus { background-position: -16px -148px; } #calendars .treelist div a.remove:focus, #calendars .treelist div a.quickview:focus, #calendars .treelist div a.subscribed:focus { border-radius: 3px; outline: 2px solid rgba(30,150,192, 0.5); } #calendars .treelist div a.remove, #calendars .treelist div a.quickview { display: inline-block; width: 16px; height: 16px; margin-right: 4px; padding: 0; background: url(images/calendars.png) -100px 0 no-repeat; overflow: hidden; text-indent: -5000px; cursor: pointer; } #calendars .treelist div a.quickview:focus, #calendars .treelist div:hover a.quickview { background-position: 0 -128px; background-color: transparent !important; } #calendars .treelist div.focusview a.quickview { background-position: -16px -128px; } #calendars .treelist div a.remove:focus, #calendars .treelist div:hover a.remove { background-position: -16px -168px; background-color: transparent !important; } #calendars .searchresults .treelist div a.remove { display: none; } #calendars .treelist li input { position: absolute; top: 5px; left: 18px; } #calendars .treelist li div.treetoggle { top: 8px; } #calendars .treelist li.virtual div.treetoggle { top: 6px; } #calendars .treelist.flat li input { left: 4px; } #calendars .treelist ul li div.folder, #calendars .treelist ul li div.calendar { margin-left: 16px; } #calendars .treelist ul ul li div.folder, #calendars .treelist ul ul li div.calendar { margin-left: 32px; } #calendars .treelist ul ul ul li div.folder, #calendars .treelist ul ul ul li div.calendar { margin-left: 48px; } #calendars .treelist li.selected > div.calendar { background-color: #c7e3ef; } #calendars .treelist li.selected > span.calname { font-weight: bold; } #calendars .treelist div.readonly span.calname { background-position: right -20px; } #calendars .treelist li.user > div > span.calname { background-position: right -38px; } /* #calendars .treelist div.user.readonly span.calname { background-position: right -56px; } #calendars .treelist div.shared span.calname { background-position: right -74px; } #calendars .treelist div.shared.readonly span.calname { background-position: right -92px; } */ #calendars .treelist .calendar .count { position: absolute; display: inline-block; top: 5px; right: 68px; min-width: 1.3em; padding: 2px 4px; 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(to bottom, #005d76 0%, #004558 100%); -webkit-box-shadow: inset 0 1px 1px 0 #002635; box-shadow: inset 0 1px 1px 0 #002635; border-radius: 10px; color: #fff; text-align: center; font-style: normal; font-weight: bold; text-shadow: none; z-index: 3; } #calendars .searchresults { background: #b0ccd7; margin-top: 8px; } #calendars .searchresults .boxtitle { background: none; padding: 2px 8px; border-radius: 0; } #calendars .searchresults .listing li { background-color: #c7e3ef; } #calfeedurl, #caldavurl { width: 98%; background: #fbfbfb; padding: 4px; margin-bottom: 1em; resize: none; } #agendalist { width: 100%; margin: 0 auto; margin-top: 60px; border: 1px solid #C1DAD7; display: none; } #agendalist table { width: 100%; } #agendalist td, #agendalist th { border-right: 1px solid #C1DAD7; border-bottom: 1px solid #C1DAD7; background: #fff; padding: 6px 6px 6px 12px; } #agendalist tr { vertical-align: top; } #agendalist th { font-weight: bold; } #calendartoolbar { position: absolute; top: -6px; left: 0; height: 40px; white-space: nowrap; } #calendartoolbar a.button { background-image: url(images/toolbar.png); padding-left: 0; padding-right: 0; min-width: 50px; max-width: 60px; } #calendartoolbar a.button.addevent { background-position: center 1px; max-width: 70px; } #calendartoolbar a.button.export { background-position: center -40px; } #calendartoolbar a.button.import { background-position: center -440px; } #calendartoolbar a.button.print { background-position: center -80px; } body.calendarmain #quicksearchbar { z-index: 20; } body.calendarmain #searchmenulink { width: 15px; } #eventedit.uidialog, .calendarmain div.uidialog { display: none; } #user { position: absolute; top: 10px; right: 100px; left: 100px; text-align: center; } a.morelink { font-size: 90%; color: #0069a6; text-decoration: none; } a.morelink:hover { text-decoration: underline; } a.miniColors-trigger { margin-top: -3px; } .calendar.attachmentwin #attachmenttoolbar { position: relative; top: -6px; height: 40px; } .calendar.attachmentwin #attachmentcontainer { position: absolute; top: 0; left: 232px; right: 0; bottom: 0; } .calendar.attachmentwin #attachmentframe { width: 100%; height: 100%; border: 0; background-color: #fff; border-radius: 4px; } .calendar.attachmentwin #partheader { position: absolute; top: 0; left: 0; width: 220px; bottom: 0; } .calendar.attachmentwin #partheader table { table-layout: fixed; overflow: hidden; } .calendar.attachmentwin #partheader table td { color: #666; padding: 4px 6px; text-overflow: ellipsis; overflow: hidden; } .calendar.attachmentwin #partheader table td.header { font-weight: bold; } .calendar.attachmentwin #partheader table td.title { width: 60px; padding-right: 0; } #edit-attachments { margin: 0.6em 0; } #edit-attachments ul li { display: block; color: #333; font-weight: bold; padding: 4px 4px 3px 30px; text-shadow: 0px 1px 1px #fff; text-decoration: none; white-space: nowrap; line-height: 20px; } #edit-attachments ul li a.file { padding: 0; } #edit-attachments-form { margin-top: 1em; padding-top: 0.8em; border-top: 2px solid #fafafa; } #edit-attachments-form .buttons { margin: 0.5em 0; } #eventedit .droptarget { background-image: url(../../../../skins/larry/images/filedrop.png) !important; background-position: center bottom !important; background-repeat: no-repeat !important; } #eventedit .droptarget.hover, #eventedit .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); } #eventedit .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); } #event-attachments .attachmentslist li { float: left; margin-right: 1em; } #event-attachments .attachmentslist li a { outline: none; } #event-panel-attachments.disabled .attachmentslist li a.delete { visibility: hidden; } .event-attendees span.attendee { padding: 1px 18px 1px 0; margin-right: 0.5em; background: url(images/attendee-status.png) right 0 no-repeat; } .event-attendees span.attendee a.mailtolink { text-decoration: none; white-space: nowrap; outline: none; } .event-attendees span.attendee a.mailtolink:hover { text-decoration: underline; } .event-attendees span.accepted { background-position: right -20px; } .event-attendees span.declined { background-position: right -40px; } .event-attendees span.tentative { background-position: right -60px; } .event-attendees span.delegated { background-position: right -180px; } .event-attendees span.organizer { background-position: right -80px; } #all-event-attendees span.attendee { display: block; margin-bottom: 0.4em; padding-bottom: 0.3em; border-bottom: 1px solid #ddd; } .calendarmain .fc-view-table td.fc-list-header, #attendees-freebusy-table h3.boxtitle, #schedule-freebusy-times thead th, .edit-attendees-table thead th { color: #69939e; font-size: 11px; font-weight: bold; background: #d6eaf3; background: -moz-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%); background: -webkit-gradient(linear, left top, right top, color-stop(0,#e3f2f6), color-stop(8%,#d6eaf3), color-stop(100%,#d6eaf3)); background: -o-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%); background: -ms-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px ,#d6eaf3 100%); background: linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%); border: 0; border-bottom: 1px solid #ccc; height: 18px; line-height: 18px; padding: 8px 7px 3px 7px; } /* jQuery UI overrides */ .calendarmain .eventdialog h1 { font-size: 18px; margin: -0.3em 0 0.4em 0; } .calendarmain .eventdialog label, .calendarmain .eventdialog h5.label { font-weight: normal; font-size: 1em; color: #999; margin: 0 0 0.2em 0; } .calendarmain .eventdialog label span.index, .calendarmain .eventdialog h5.label .index { vertical-align: inherit; margin-left: 0.6em; } .calendarmain .eventdialog { margin: 0 -0.2em; } #event-status-badge { width: 100px; height: 100px; position: absolute; top: 0; right: 0; overflow: hidden; } #event-status-badge span { display: none; text-transform: uppercase; width: 150px; height: 20px; line-height: 20px; position: absolute; left: -20px; top: 35px; padding-left: 10px; text-align: center; font-weight: bold; font-size: 12px; color: #fff; box-shadow: 1px 1px 2px #ccc, -1px -1px 2px #ccc; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); } .eventdialog.status-cancelled #event-status-badge span { background: url(images/badge.png) 26px -24px no-repeat #cc0000; display: block; } .eventdialog.sensitivity-private #event-status-badge span { background: url(images/badge.png) 40px -52px no-repeat #0066ff; display: block; } .eventdialog.sensitivity-confidential #event-status-badge span { background: url(images/badge.png) 20px 2px no-repeat #cc0000; display: block; } .calendarmain .status-cancelled #event-title, .calendarmain .sensitivity-private #event-title, .calendarmain .sensitivity-confidential #event-title { margin-right: 80px; } .calendarmain .eventdialog div.event-line { margin-top: 0.1em; margin-bottom: 0.3em; } .calendarmain .eventdialog div.event-line a.iconbutton { margin-left: 0.5em; line-height: 17px; } .calendarmain .eventdialog div.event-line span.event-text + label { margin-left: 2em; } .eventdialog .event-text-old, .eventdialog .event-text-new, .eventdialog .event-text-diff { padding: 2px; } .eventdialog .event-text-diff del, .eventdialog .event-text-diff ins { text-decoration: none; color: inherit; } .eventdialog .event-text-old, .eventdialog .event-text-diff del { background-color: #fdd; /* text-decoration: line-through; */ } .eventdialog .event-text-new, .eventdialog .event-text-diff ins { background-color: #dfd; } #eventdiff .attachmentslist li a, #eventdiff .attachmentslist li a:hover { cursor: default; text-decoration: none; } .changelog-table .loading { color: #666; margin: 1em 0; padding: 1px 0 2px 24px; background: url(images/loading_blue.gif) top left no-repeat; } .changelog-dialog .compare-button { margin: 4px 0; } .changelog-table tbody td { padding: 4px 7px; vertical-align: middle; } .changelog-table tbody tr:last-child td { border-bottom: 0; } .changelog-table tbody tr.undisclosed td.date, .changelog-table tbody tr.undisclosed td.user { font-style: italic; } .changelog-table .diff { width: 4em; padding: 2px; } .changelog-table .revision { width: 6em; } .changelog-table .date { width: 11em; } .changelog-table .user { width: auto; } .changelog-table .operation { width: 15%; } .changelog-table .actions { width: 50px; text-align: right; padding: 4px; } .changelog-table td a.iconbutton.restore, .changelog-table td a.iconbutton.preview { width: 16px; margin-right: 2px; background-image: url(images/calendars.png); background-position: -1px -147px; } .changelog-table td a.iconbutton.restore { background-image: url(images/calendars.png); background-position: -1px -167px; } .changelog-table tr.first td a.iconbutton { opacity: 0.3; cursor: default; } #event-partstat .changersvp { cursor: pointer; color: #333; text-decoration: none; } #event-partstat .iconbutton { visibility: hidden; } #event-partstat .changersvp:focus .iconbutton, #event-partstat:hover .iconbutton { visibility: visible; } #eventedit { position: relative; top: -1.5em; padding: 0.5em 0.1em; margin: 0 -0.2em; } #eventedit input.text, #eventedit textarea { width: 97%; } #eventtabs { position: relative; padding: 0; border: 0; border-radius: 0; } div.form-section, .calendarmain .eventdialog div.event-section, #eventtabs div.event-section { margin-top: 0.2em; margin-bottom: 0.6em; } #eventtabs .border-after { padding-bottom: 0.8em; margin-bottom: 0.8em; border-bottom: 2px solid #fafafa; } .calendarmain .eventdialog label, #eventedit label, .form-section label { display: inline-block; min-width: 7em; padding-right: 0.5em; } .calendarmain .eventdialog #event-url .event-text { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } #event-links .attachmentslist { display: inline-block; } #event-links label, #edit-event-links label { float: left; margin-top: 0.3em; padding-right: 0.75em; } #edit-event-links .event-text { margin-left: 8em; min-height: 22px; } #edit-event-links .attachmentslist li.message a.messagelink, #event-links .attachmentslist li.message a.messagelink { padding: 0 0 0 24px; } #edit-event-links .attachmentslist li a.delete { top: 0; background-position: -6px -378px; } #edit-event-links .attachmentslist li.deleted a.messagelink, #edit-event-links .attachmentslist li.deleted a.messagelink:hover { text-decoration: line-through; } #eventedit .formtable td.label { min-width: 6em; } td.topalign { vertical-align: top; } #eventedit label.weekday, #eventedit label.monthday { min-width: 3em; } #eventedit label.month { min-width: 5em; } #eventedit .formtable td { padding: 0.2em 0; } .ui-dialog .event-update-confirm { padding: 0 0.5em 0.5em 0.5em; } .event-dialog-message, .event-update-confirm .message { margin-top: 0.5em; padding: 0.8em; border: 1px solid #ffdf0e; background-color: #fef893; } .event-dialog-message .message, .event-update-confirm .message { margin-bottom: 0.5em; } .edit-recurring-warning .savemode { padding-left: 20px; } .event-update-confirm .savemode { padding-left: 30px; } .event-dialog-message span.ui-icon, .event-update-confirm span.ui-icon { float: left; margin: 0 7px 20px 0; } .event-dialog-message label, .event-update-confirm label { min-width: 3em; padding-right: 1em; } .event-update-confirm a.button { margin: 0 0.5em 0 0.2em; min-width: 5em; text-align: center; } .libcal-rsvp-replymode li a { cursor: default; } #event-rsvp, #edit-attendees-notify { margin: 0.6em 0 0.3em 0; padding: 0.5em; } #event-rsvp .itip-reply-controls { margin-top: 0.5em; } #event-rsvp .itip-reply-controls label { color: #333; } #event-rsvp .itip-reply-controls textarea { width: 95%; } #eventedit .edit-attendees-table { width: 100%; margin-top: 0.5em; } #eventedit .edit-attendees-table th.role, #eventedit .edit-attendees-table td.role { width: 9em; } #eventedit .edit-attendees-table th.availability, #eventedit .edit-attendees-table td.availability, #eventedit .edit-attendees-table th.confirmstate, #eventedit .edit-attendees-table td.confirmstate { width: 4em; } #eventedit .edit-attendees-table th.options, #eventedit .edit-attendees-table td.options { width: 16px; padding: 2px 4px; } #eventedit .edit-attendees-table th.invite, #eventedit .edit-attendees-table td.invite { width: 50px; padding: 2px; } #eventedit .edit-attendees-table th.invite label { display: inline-block; position: relative; top: 4px; width: 24px; height: 18px; min-width: 24px; padding: 0; overflow: hidden; text-indent: -5000px; white-space: nowrap; background: url(images/sendinvitation.png) 1px 0 no-repeat; } #eventedit .edit-attendees-table tbody tr:last-child td { border-bottom: 0; } #eventedit .edit-attendees-table th.name, #eventedit .edit-attendees-table td.name { width: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #eventedit .edit-attendees-table td.name select { width: 100%; } #eventedit .edit-attendees-table td.name .attendee-name { display: block; position: relative; overflow: hidden; text-overflow: ellipsis; padding: 5px 7px 6px; margin: -5px -7px -6px; } #eventedit .edit-attendees-table a.deletelink { display: inline-block; width: 17px; height: 17px; padding: 0; overflow: hidden; text-indent: 1000px; } #eventedit .edit-attendees-table a.expandlink { position: absolute; top: 4px; right: 6px; width: 16px; height: 16px; } #edit-attendees-form, #edit-resources-form { position: relative; margin-top: 15px; } #edit-attendees-form .attendees-invitebox { text-align: right; margin: 0; } #edit-attendees-form .attendees-invitebox label { padding-right: 3px; } #edit-resources-form #edit-resource-find { position: absolute; top: 0; right: 0; } #edit-attendees-form #edit-attendee-schedule { position: absolute; right: 0; top: 0; } .edit-attendees-table select.edit-attendee-role { border: 0; padding: 2px; background: white; width: 100%; } .availability img.availabilityicon { margin: 1px; width: 14px; height: 14px; border-radius: 4px; -moz-border-radius: 4px; vertical-align: middle; } .availability img.availabilityicon.loading { background: url(images/loading_blue.gif) center no-repeat; } #schedule-freebusy-times td div.unknown, .availability img.availabilityicon.unknown { background: #ddd; } #schedule-freebusy-times td div.free, .availability img.availabilityicon.free { background: #abd640; } #schedule-freebusy-times td div.busy, .availability img.availabilityicon.busy { background: #e26569; } #schedule-freebusy-times td div.tentative, .availability img.availabilityicon.tentative { background: #8383fc; } #schedule-freebusy-times td div.out-of-office, .availability img.availabilityicon.out-of-office { background: #fbaa68; } #schedule-freebusy-times td div.all-busy, #schedule-freebusy-times td div.all-tentative, #schedule-freebusy-times td div.all-out-of-office { background-image: url(images/freebusy-colors.png); background-position: top right; background-repeat: no-repeat; } #schedule-freebusy-times td div.all-tentative { background-position: right -40px; } #schedule-freebusy-times td div.all-out-of-office { background-position: right -80px; } #edit-attendees-legend { margin-top: 3em; margin-bottom: 0.5em; } #edit-attendees-legend .legend { margin-right: 2em; white-space: nowrap; } .edit-attendees-table tbody td.confirmstate { overflow: hidden; white-space: nowrap; text-indent: -2000%; } .edit-attendees-table td.confirmstate span { display: block; width: 20px; background: url(images/attendee-status.png) 5px 0 no-repeat; } .edit-attendees-table td.confirmstate span.needs-action { height: 14px; } .edit-attendees-table td.confirmstate span.accepted { background-position: 5px -20px; height: 14px; } .edit-attendees-table td.confirmstate span.declined { background-position: 5px -40px; height: 14px; } .edit-attendees-table td.confirmstate span.tentative { background-position: 5px -60px; height: 14px; } .edit-attendees-table td.confirmstate span.delegated { background-position: 5px -180px; height: 14px; } #attendees-freebusy-table { width: 100%; table-layout: fixed; border: 1px solid #bbd3da; } #attendees-freebusy-table td.attendees { width: 18em; vertical-align: top; overflow: hidden; } #attendees-freebusy-table td.times { width: auto; vertical-align: top; } #attendees-freebusy-table div.scroll { position: relative; overflow: auto; } #attendees-freebusy-table h3.boxtitle { margin: 0; border-color: #ccc; } .attendees-list .attendee { padding: 4px 4px 4px 1px; background: url(images/attendee-status.png) 2px -97px no-repeat; white-space: nowrap; } .attendees-list a.attendee-role-toggle { display: inline-block; width: 16px; margin-right: 3px; cursor: pointer; } .attendees-list div.attendee { border-top: 1px solid #ccc; } .attendees-list span.attendee { padding-left: 20px; margin-right: 2em; } .attendees-list .organizer { background-position: 3px -77px; } .attendees-list .opt-participant { background-position: 2px -117px; } .attendees-list .non-participant { background-position: 2px -137px; } .attendees-list .chair { background-position: 2px -157px; } .attendees-list .loading { background: url(images/loading_blue.gif) 1px 50% no-repeat; } .attendees-list .total { background: none; padding-left: 4px; font-weight: bold; } .attendees-list .spacer, #schedule-freebusy-times tr.spacer td { background: 0; padding: 0; height: 10px; } #schedule-freebusy-times { border-collapse: collapse; width: 100%; } #schedule-freebusy-times td { padding: 4px; border: 1px solid #ccc; } #schedule-freebusy-times tbody td { padding: 0; height: 20px; } #schedule-freebusy-times tbody td div { height: 100%; } #attendees-freebusy-table div.timesheader, #schedule-freebusy-times tr.times td { min-width: 30px; font-size: 9px; padding: 5px 2px 6px 2px; text-align: center; color: #004658; } #schedule-freebusy-times tr.times td.allday { min-width: 60px; } #schedule-freebusy-times tr.times td { cursor: pointer; } #schedule-freebusy-times #fbrowall td { border-bottom: none; } #schedule-event-time { position: absolute; border: 2px solid #333; background: #777; background: rgba(60, 60, 60, 0.6); opacity: 0.5; border-radius: 4px; cursor: move; filter: alpha(opacity=40); /* IE8 */ } #eventfreebusy .schedule-options { position: relative; margin-bottom: 1.5em; } #eventfreebusy .schedule-buttons { position: absolute; top: 0.5em; right: 0; margin-right: 0; } #eventfreebusy .schedule-find-buttons { padding-bottom:0.5em; } #eventfreebusy .schedule-find-buttons button { min-width: 9em; text-align: center; } #eventedit .attendees-commentbox label { display: block; } #eventedit .ui-tabs-panel { min-height: 24em; } #rcmKSearchpane ul li.resource i.icon, #rcmKSearchpane ul li.collection i.icon { background-image: url(images/autocomplete.png); background-position: -1px -2px; } #rcmKSearchpane ul li.collection i.icon { background-position: -1px -26px; } a.dropdown-link { font-size: 11px; text-decoration: none; } a.dropdown-link:after { content: ' ▼'; font-size: 10px; color: #666; } .ui-dialog-buttonset a.dropdown-link { position: relative; top: 2px; margin: 0 1em; color: #333; } #calendarsidebar .ui-datepicker-calendar { table-layout: fixed; } .ui-datepicker-calendar .ui-datepicker-week-col { border: 0; color: #999; font-size: 90%; text-align: right; padding-right: 6px; width: 20px; overflow: hidden; } .ui-autocomplete { max-height: 160px; overflow-y: auto; overflow-x: hidden; } .ui-autocomplete .ui-menu-item { white-space: nowrap; } * html .ui-autocomplete { height: 160px; } .calendarmain span.spacer { padding-left: 3em; } #agendaoptions { position: absolute; bottom: 0; left: 0; right: 0; height: auto; z-index: 10; padding: 4px 5px; border: 1px solid #c3c3c3; border-top-color: #ddd; border-bottom-color: #bbb; border-radius: 0 0 4px 4px; background: #ebebeb; background: -moz-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ebebeb), color-stop(100%,#c6c6c6)); background: -o-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%); background: -ms-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%); background: linear-gradient(top, #ebebeb 0%, #c6c6c6 100%); } #agendaoptions label { text-shadow: 1px 1px #fff; padding-right: 0.5em; } #calendar-kolabform { position: relative; margin: 0 -8px; min-width: 660px; min-height: 400px; } #calendar-kolabform table td.title { font-weight: bold; white-space: nowrap; color: #666; padding-right: 10px; } #resource-selection { position: absolute; top: 0; left: 8px; right: 0; bottom: 0; } #resource-selection .scroller { top: 34px; } #resource-dialog-left { position: absolute; top: 10px; left: 0; width: 380px; bottom: 10px; } #resource-dialog-right { position: absolute; top: 10px; left: 392px; right: 8px; bottom: 10px; } #resource-info { position: absolute; top: 0; left: 0; right: 0; height: 48%; } #resource-info table { margin: 8px; width: 97%; } #resource-info thead td { background: none; font-weight: bold; font-size: 14px; } #resource-availability { position: absolute; bottom: 0; left: 0; right: 0; height: 49%; } #resource-freebusy-calendar { position: absolute; top: 33px; left: -1px; right: -1px; bottom: -1px; } #resource-freebusy-calendar .fc-content { top: 0; } #resource-freebusy-calendar .fc-content .fc-event-bg { background: 0; } #resource-freebusy-calendar .fc-event.status-busy, #resource-freebusy-calendar .status-busy .fc-event-skin { border-color: #e26569; background-color: #e26569; } #resource-freebusy-calendar .fc-event.status-tentative, #resource-freebusy-calendar .status-tentative .fc-event-skin { border-color: #8383fc; background: #8383fc; } #resource-freebusy-calendar .fc-event.status-outofoffice, #resource-freebusy-calendar .status-outofoffice .fc-event-skin { border-color: #fbaa68; background: #fbaa68; } #resourcequicksearch { padding: 4px; background: #c7e3ef; } #resourcesearchbox { width: 100%; height: 26px; -moz-box-sizing: border-box; box-sizing: border-box; } #resourcequicksearch .iconbutton.searchoptions { position: absolute; top: 5px; left: 6px; width: 16px; } .searchbox .iconbutton.reset { position: absolute; top: 4px; right: 1px; } /* fullcalendar style overrides */ .rcube-fc-content { overflow: hidden; border: 0; border-radius: 4px; box-shadow: 0 0 2px #999; -o-box-shadow: 0 0 2px #999; -webkit-box-shadow: 0 0 2px #999; -moz-box-shadow: 0 0 2px #999; } .calendarmain .fc-content { position: absolute !important; top: 40px; left: 0; right: 0; bottom: 0; background: #fff; } .calendarmain.quickview-active .fc-content { background-image: url('images/focusview.png'); background-position: center; background-repeat: no-repeat; } #fish-eye-view .fc-content { top: 2px; bottom: 2px; } #quickview-calendar { padding: 8px; overflow: hidden; } .calendarmain .fc-button, .calendarmain .fc-button.fc-state-default, .calendarmain .fc-button.fc-state-hover { background-color: #f5f5f5; background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); background-position: 0 0; } .calendarmain #calendar .fc-button, .calendarmain #calendar .fc-button.fc-state-default, .calendarmain #calendar .fc-button.fc-state-hover { margin: 0 0 0 0; height: 20px; line-height: 20px; color: #505050; text-shadow: 0px 1px 1px #fff; border: 1px solid #e6e6e6; background: #d8d8d8; background: -moz-linear-gradient(top, #d8d8d8 0%, #bababa 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d8d8d8), color-stop(100%,#bababa)); background: -o-linear-gradient(top, #d8d8d8 0%, #bababa 100%); background: -ms-linear-gradient(top, #d8d8d8 0%, #bababa 100%); background: linear-gradient(top, #d8d8d8 0%, #bababa 100%); box-shadow: 0 1px 1px 0 #999; -o-box-shadow: 0 1px 1px 0 #999; -webkit-box-shadow: 0 1px 1px 0 #999; -moz-box-shadow: 0 1px 1px 0 #999; text-decoration: none; } .calendarmain #calendar .fc-button.fc-state-disabled { color: #999; background: #d8d8d8; } .calendarmain .fc-button.fc-state-active, .calendarmain .fc-button.fc-state-down, .calendarmain #calendar .fc-button.fc-state-active, .calendarmain #calendar .fc-button.fc-state-down { color: #333; background: #bababa; background: -moz-linear-gradient(top, #bababa 0%, #d8d8d8 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bababa), color-stop(100%,#d8d8d8)); background: -o-linear-gradient(top, #bababa 0%, #d8d8d8 100%); background: -ms-linear-gradient(top, #bababa 0%, #d8d8d8 100%); background: linear-gradient(top, #bababa 0%, #d8d8d8 100%); } .calendarmain #calendar .fc-header .fc-button { margin-left: -1px; margin-right: 0; } .calendarmain #calendar .fc-header-left .fc-button { display: inline-block; margin: 0; text-align: center; font-size: 10px; color: #555; min-width: 50px; max-width: 75px; height: 13px; line-height: 1em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: -7px 0 0 0; padding: 28px 2px 0 2px; text-shadow: 0px 1px 1px #EEE; border: 0; background: url(images/toolbar.png) center 100px no-repeat; box-shadow: none; -o-box-shadow: none; -webkit-box-shadow: none; -moz-box-shadow: none; outline: none; } .calendarmain #calendar .fc-header-left .fc-button:focus { color: #fff; text-shadow: 0px 1px 1px #666; background-color: rgba(30,150,192, 0.5); border-radius: 3px; } .calendarmain #calendar .fc-header-left .fc-button.fc-state-active { font-weight: bold; color: #222; text-shadow: none; background-color: transparent; } .calendarmain #calendar .fc-header-left .fc-button-agendaDay { background-position: center -120px; } .calendarmain #calendar .fc-header-left .fc-button-agendaDay.fc-state-active { background-position: center -160px; } .calendarmain #calendar .fc-header-left .fc-button-agendaWeek { background-position: center -200px; } .calendarmain #calendar .fc-header-left .fc-button-agendaWeek.fc-state-active { background-position: center -240px; } .calendarmain #calendar .fc-header-left .fc-button-month { background-position: center -280px; } .calendarmain #calendar .fc-header-left .fc-button-month.fc-state-active { background-position: center -320px; } .calendarmain #calendar .fc-header-left .fc-button-table { background-position: center -360px; } .calendarmain #calendar .fc-header-left .fc-button-table.fc-state-active { background-position: center -400px; } .calendarmain #calendar .fc-header-right { padding-right: 252px; padding-top: 4px; } .calendarmain #calendar .fc-header-title { padding-top: 5px; } .fc-event { font-size: 1em !important; } .fc-event-hori.fc-type-freebusy, .fc-event-vert.fc-type-freebusy { opacity: 0.60; /* color: #fff !important; background: rgba(80,80,80,0.85) !important; background: -moz-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.9) 100%) !important; background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(80,80,80,0.85)), color-stop(100%,rgba(48,48,48,0.9))) !important; background: -webkit-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important; background: -o-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important; background: -ms-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important; background: linear-gradient(to bottom, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important; border-color: #444 !important; cursor: default !important; */ -moz-box-shadow: inset 0px 1px 0 0px #888; -webkit-box-shadow: inset 0px 1px 0 0px #888; -o-box-shadow: inset 0px 1px 0 0px #888; box-shadow: inset 0px 1px 0 0px #888; } .fc-event-row.fc-type-freebusy td { color: #999; } .fc-event-hori.fc-type-freebusy .fc-event-skin, .fc-event-hori.fc-type-freebusy .fc-event-inner, .fc-event-vert.fc-type-freebusy .fc-event-skin, .fc-event-vert.fc-type-freebusy .fc-event-inner { /* background-color: transparent !important; border-color: #444 !important; color: #fff !important; text-shadow: 0 1px 1px #000; */ } .fc-event-hori.fc-type-freebusy .fc-event-title, .fc-event-vert.fc-type-freebusy .fc-event-title { position: absolute; top: -5000px; } .fc-event-vert.fc-invitation-needs-action, .fc-event-hori.fc-invitation-needs-action { border: 1px dashed #5757c7 !important; } .fc-event-vert.fc-invitation-tentative, .fc-event-hori.fc-invitation-tentative { border: 1px dashed #eb8900 !important; } .fc-event-vert.fc-invitation-declined, .fc-event-hori.fc-invitation-declined { border: 1px dashed #c00 !important; } .fc-event-vert.fc-event-ns-other.fc-invitation-declined, .fc-event-hori.fc-event-ns-other.fc-invitation-declined { opacity: 0.7; } .fc-event-ns-other.fc-invitation-declined .fc-event-title { text-decoration: line-through; } .fc-event-vert.fc-invitation-tentative .fc-event-head, .fc-event-vert.fc-invitation-declined .fc-event-head, .fc-event-vert.fc-invitation-needs-action .fc-event-head { /* background-color: transparent !important; */ } .fc-event-vert.fc-invitation-tentative .fc-event-bg { background: url(data:image/gif;base64,R0lGODlhCAAIAPABAOuJAP///yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff; } .fc-event-vert.fc-invitation-needs-action .fc-event-bg { background: url(data:image/gif;base64,R0lGODlhCAAIAPABAFdXx////yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff; } .fc-event-vert.fc-invitation-declined .fc-event-bg { background: url(data:image/gif;base64,R0lGODlhCAAIAPABAMwAAP///yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff; } .fc-view-table tr.fc-invitation-tentative td, .fc-view-table tr.fc-invitation-declined td, .fc-view-table tr.fc-invitation-needs-action td { color: #888; } .fc-view-table tr.fc-invitation-tentative td.fc-event-title, .fc-view-table tr.fc-invitation-declined td.fc-event-title, .fc-view-table tr.fc-invitation-needs-action td.fc-event-title { font-weight: normal; } #quickview-calendar .fc-view-table tr.fc-invitation-tentative td, #quickview-calendar .fc-view-table tr.fc-invitation-declined td, #quickview-calendar .fc-view-table tr.fc-invitation-needs-action td { color: #333; } .calendarmain .fc-event:focus { outline: 1px solid rgba(71,135,177, 0.4); -webkit-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6); -moz-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6); -o-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6); box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6); } .fc-event-title { font-weight: bold; } .cal-event-status-cancelled .fc-event-title { text-decoration: line-through; } .fc-event-hori .fc-event-title { font-weight: normal; white-space: nowrap; } .fc-event-hori .fc-event-time { white-space: nowrap; font-weight: normal !important; font-size: 10px; padding-right: 0.6em; } .fc-grid .fc-event-time { font-weight: normal !important; padding-right: 0.3em; } .calendarmain .fc-event-vert .fc-event-inner { z-index: 0; } .fc-event-cateories { font-style:italic; } div.fc-event-location { font-size: 90%; } .fc-more-link { color: #999; padding-top: 1px; cursor: pointer; } .fc-agenda-slots td div { height: 22px; } .fc-sat, .fc-sun { background-color: rgba(198,198,198, 0.08); } .calendarmain .fc-state-highlight { background-color: rgba(233,198,14, 0.12); } .fc-widget-header { background-color: #d6eaf3; color: #004458; text-shadow: 0px 1px 1px #fff; } .fc-view thead th.fc-widget-header { padding: 8px 0; color: #69939e; } .fc-day-number { color: #578da5; } .fc-icon-alarms, .fc-icon-sensitive, .fc-icon-recurring { display: inline-block; width: 11px; height: 11px; background: url(images/eventicons.png) 0 0 no-repeat; margin-left: 3px; line-height: 10px; } .fc-icon-alarms { background-position: 0 -13px; } .fc-icon-sensitive { background-position: 0 -25px; } .fc-list-section .fc-event { cursor: pointer; } .calendarmain .fc-view-table td.fc-list-header { color: #004458; font-size: 12px; } .calendarmain .fc-view-table tr.fc-event td { border-color: #ddd; padding: 4px 7px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .calendarmain .fc-view-table tr.fc-event td.fc-event-handle { padding: 5px 0 2px 7px; width: 12px; } .calendarmain .fc-view-table .fc-event-handle .fc-event-skin { margin: 0; padding: 0; display: inline-block; width: 10px; height: 10px; font-size: 6px; border-radius: 8px; } .calendarmain .fc-view-table .fc-event-handle .fc-event-inner { display: inline-block; width: 10px; height: 10px; padding: 0; margin: -1px; font-size: 10px; border-radius: 8px; border: 1px solid rgba(0, 0, 0, 0.4); -webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3); -moz-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3); box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3); } .calendarmain .fc-view-table col.fc-event-location { width: 25%; } .fc-view-table table.fc-list-smart { /* table-layout: auto; */ } .fc-listappend { text-align: center; margin: 1em 0; } .fc-listappend .message { padding: 0.5em; margin-bottom: 0.5em; font-size: 150%; color: #999; } .fc-listappend .formlinks a { font-size: 12px; padding: 0 0.3em; } .fc-event-temp { opacity: 0.4; filter: alpha(opacity=40); /* IE8 */ } /* Settings section */ fieldset #calendarcategories div { margin-bottom: 0.3em; } /* Invitation UI in mail */ .messagelist tbody .attachment span.ical { display: inline-block; vertical-align: middle; height: 18px; width: 20px; padding: 0; background: url(images/ical-attachment.png) 2px 1px no-repeat; } ul.toolbarmenu li a.calendarlink span.calendar, #attachmentmenu li a.calendarlink span.calendar { background-position: 0px -2197px; } div.calendar-invitebox { min-height: 20px; margin: 5px 8px; padding: 3px 6px 6px 34px; border: 1px solid #ffdf0e; background: url(images/calendar.png) 6px 5px no-repeat #fef893; } div.calendar-invitebox td.ititle { font-weight: bold; padding-right: 0.5em; } div.calendar-invitebox td { padding: 2px; } div.calendar-invitebox td.label { color: #666; padding-right: 1em; } div.calendar-invitebox td.sensitivity { color: #d31400; font-weight: bold; } div.calendar-invitebox td.recurrence-id { text-transform: uppercase; font-style: italic; } div.calendar-invitebox td em { font-weight: bold; } +div.calendar-invitebox td.date.modified { + font-weight: bold; + color: red; +} + #event-rsvp .rsvp-buttons, div.calendar-invitebox .itip-buttons div { margin-top: 0.5em; } #event-rsvp input.button, div.calendar-invitebox input.button { font-weight: bold; margin-right: 0.5em; } div.calendar-invitebox input.button.preview { margin-left: 1em; margin-right: 0; } div.calendar-invitebox .folder-select { font-weight: 10px; margin-left: 1em; white-space: nowrap; } div.calendar-invitebox .rsvp-status { padding-left: 2px; } div.calendar-invitebox .rsvp-status.loading { color: #666; padding: 1px 0 2px 24px; background: url(images/loading_blue.gif) top left no-repeat; } div.calendar-invitebox .rsvp-status.hint { color: #666; text-shadow: none; font-style: italic; } #event-partstat .changersvp, div.calendar-invitebox .rsvp-status.declined, div.calendar-invitebox .rsvp-status.tentative, div.calendar-invitebox .rsvp-status.accepted, div.calendar-invitebox .rsvp-status.delegated, div.calendar-invitebox .rsvp-status.needs-action { padding: 0 0 1px 22px; background: url(images/attendee-status.png) 2px -20px no-repeat; } #event-partstat .changersvp.declined, div.calendar-invitebox .rsvp-status.declined { background-position: 2px -40px; } #event-partstat .changersvp.tentative, div.calendar-invitebox .rsvp-status.tentative { background-position: 2px -60px; } #event-partstat .changersvp.delegated, div.calendar-invitebox .rsvp-status.delegated { background-position: 2px -180px; } #event-partstat .changersvp.needs-action, div.calendar-invitebox .rsvp-status.needs-action { background-position: 2px 0; } div.calendar-invitebox .calendar-agenda-preview { display: none; border-top: 1px solid #dfdfdf; margin-top: 1em; padding-top: 0.6em; } div.calendar-invitebox .calendar-agenda-preview h3.preview-title { margin: 0 0 0.5em 0; font-size: 12px; color: #333; } div.calendar-invitebox .calendar-agenda-preview .event-row { color: #777; padding: 2px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } div.calendar-invitebox .calendar-agenda-preview .event-row.current { color: #333; font-weight: bold; } div.calendar-invitebox .calendar-agenda-preview .event-row.no-event { font-style: italic; } div.calendar-invitebox .calendar-agenda-preview .event-date { display: inline-block; min-width: 8em; margin-right: 1em; white-space: nowrap; } /* iTIP attend reply page */ .calendaritipattend .centerbox { width: 40em; min-height: 7em; margin: 80px auto 0 auto; padding: 10px 10px 10px 90px; background: url(images/invitation.png) 10px 10px no-repeat #fff; } .calendaritipattend #message { width: 46em; margin: 0 auto; padding: 10px; } .calendaritipattend .calendar-invitebox { background: none; padding-left: 0; border: 0; margin: 0 0 2em 0; } .calendaritipattend .calendar-invitebox .rsvp-status { margin-top: 2.5em; font-size: 110%; font-weight: bold; } .calendaritipattend .calendar-invitebox td.title, .calendaritipattend .calendar-invitebox td.ititle { font-size: 120%; } .calendaritipattend .itip-reply-controls .noreply-toggle, .calendaritipattend .itip-reply-controls #noreply-event-rsvp { display: none; } .calendaritipattend .itip-reply-controls a.reply-comment-toggle { margin-left: 2px; } diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php index c6eef240..c7986175 100644 --- a/plugins/libcalendaring/lib/libcalendaring_itip.php +++ b/plugins/libcalendaring/lib/libcalendaring_itip.php @@ -1,841 +1,852 @@ * * Copyright (C) 2011-2014, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ class libcalendaring_itip { protected $rc; protected $lib; protected $plugin; protected $sender; protected $domain; protected $itip_send = false; protected $rsvp_actions = array('accepted','tentative','declined','delegated'); protected $rsvp_status = array('accepted','tentative','declined','delegated'); function __construct($plugin, $domain = 'libcalendaring') { $this->plugin = $plugin; $this->rc = rcube::get_instance(); $this->lib = libcalendaring::get_instance(); $this->domain = $domain; $hook = $this->rc->plugins->exec_hook('calendar_load_itip', array('identity' => $this->rc->user->list_emails(true))); $this->sender = $hook['identity']; $this->plugin->add_hook('message_before_send', array($this, 'before_send_hook')); $this->plugin->add_hook('smtp_connect', array($this, 'smtp_connect_hook')); } public function set_sender_email($email) { if (!empty($email)) $this->sender['email'] = $email; } public function set_rsvp_actions($actions) { $this->rsvp_actions = (array)$actions; $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated')); } public function set_rsvp_status($status) { $this->rsvp_status = $status; } /** * Wrapper for rcube_plugin::gettext() * Checking for a label in different domains * * @see rcube::gettext() */ public function gettext($p) { $label = is_array($p) ? $p['name'] : $p; $domain = $this->domain; if (!$this->rc->text_exists($label, $domain)) { $domain = 'libcalendaring'; } return $this->rc->gettext($p, $domain); } /** * Send an iTip mail message * * @param array Event object to send * @param string iTip method (REQUEST|REPLY|CANCEL) * @param array Hash array with recipient data (name, email) * @param string Mail subject * @param string Mail body text label * @param object Mail_mime object with message data * @param boolean Request RSVP * @return boolean True on success, false on failure */ public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null, $rsvp = true) { if (!$this->sender['name']) $this->sender['name'] = $this->sender['email']; if (!$message) { libcalendaring::identify_recurrence_instance($event); $message = $this->compose_itip_message($event, $method, $rsvp); } $mailto = rcube_utils::idn_to_ascii($recipient['email']); $headers = $message->headers(); $headers['To'] = format_email_recipient($mailto, $recipient['name']); $headers['Subject'] = $this->gettext(array( 'name' => $subject, 'vars' => array( 'title' => $event['title'], 'name' => $this->sender['name'] ) )); // compose a list of all event attendees $attendees_list = array(); foreach ((array)$event['attendees'] as $attendee) { $attendees_list[] = ($attendee['name'] && $attendee['email']) ? $attendee['name'] . ' <' . $attendee['email'] . '>' : ($attendee['name'] ? $attendee['name'] : $attendee['email']); } $recurrence_info = ''; if (!empty($event['recurrence_id'])) { $recurrence_info = "\n\n** " . $this->gettext($event['thisandfuture'] ? 'itipmessagefutureoccurrence' : 'itipmessagesingleoccurrence') . ' **'; } else if (!empty($event['recurrence'])) { $recurrence_info = sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence'])); } $mailbody = $this->gettext(array( 'name' => $bodytext, 'vars' => array( 'title' => $event['title'], 'date' => $this->lib->event_date_text($event, true) . $recurrence_info, 'attendees' => join(",\n ", $attendees_list), 'sender' => $this->sender['name'], 'organizer' => $this->sender['name'], ) )); // if (!empty($event['comment'])) { // $mailbody .= "\n\n" . $this->gettext('itipsendercomment') . $event['comment']; // } // append links for direct invitation replies if ($method == 'REQUEST' && $rsvp && ($token = $this->store_invitation($event, $recipient['email']))) { $mailbody .= "\n\n" . $this->gettext(array( 'name' => 'invitationattendlinks', 'vars' => array('url' => $this->plugin->get_url(array('action' => 'attend', 't' => $token))), )); } else if ($method == 'CANCEL' && $event['cancelled']) { $this->cancel_itip_invitation($event); } $message->headers($headers, true); $message->setTXTBody(rcube_mime::format_flowed($mailbody, 79)); if ($this->rc->config->get('libcalendaring_itip_debug', false)) { rcube::console('iTip ' . $method, $message->txtHeaders() . "\r\n" . $message->get()); } // finally send the message $this->itip_send = true; $sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error); $this->itip_send = false; return $sent; } /** * Plugin hook triggered by rcube::deliver_message() before delivering a message. * Here we can set the 'smtp_server' config option to '' in order to use * PHP's mail() function for unauthenticated email sending. */ public function before_send_hook($p) { if ($this->itip_send && !$this->rc->user->ID && $this->rc->config->get('calendar_itip_smtp_server', null) === '') { $this->rc->config->set('smtp_server', ''); } return $p; } /** * Plugin hook to alter SMTP authentication. * This is used if iTip messages are to be sent from an unauthenticated session */ public function smtp_connect_hook($p) { // replace smtp auth settings if we're not in an authenticated session if ($this->itip_send && !$this->rc->user->ID) { foreach (array('smtp_server', 'smtp_user', 'smtp_pass') as $prop) { $p[$prop] = $this->rc->config->get("calendar_itip_$prop", $p[$prop]); } } return $p; } /** * Helper function to build a Mail_mime object to send an iTip message * * @param array Event object to send * @param string iTip method (REQUEST|REPLY|CANCEL) * @param boolean Request RSVP * @return object Mail_mime object with message data */ public function compose_itip_message($event, $method, $rsvp = true) { $from = rcube_utils::idn_to_ascii($this->sender['email']); $from_utf = rcube_utils::idn_to_utf8($from); $sender = format_email_recipient($from, $this->sender['name']); // truncate list attendees down to the recipient of the iTip Reply. // constraints for a METHOD:REPLY according to RFC 5546 if ($method == 'REPLY') { $replying_attendee = null; $reply_attendees = array(); foreach ($event['attendees'] as $attendee) { if ($attendee['role'] == 'ORGANIZER') { $reply_attendees[] = $attendee; } else if (strcasecmp($attendee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) { $replying_attendee = $attendee; if ($attendee['status'] != 'DELEGATED') { unset($replying_attendee['rsvp']); // unset the RSVP attribute } } // include attendees relevant for delegation (RFC 5546, Section 4.2.5) else if ((!empty($attendee['delegated-to']) && (strcasecmp($attendee['delegated-to'], $from) == 0 || strcasecmp($attendee['delegated-to'], $from_utf) == 0)) || (!empty($attendee['delegated-from']) && (strcasecmp($attendee['delegated-from'], $from) == 0 || strcasecmp($attendee['delegated-from'], $from_utf) == 0))) { $reply_attendees[] = $attendee; } } if ($replying_attendee) { array_unshift($reply_attendees, $replying_attendee); $event['attendees'] = $reply_attendees; } if ($event['recurrence']) { unset($event['recurrence']['EXCEPTIONS']); } } // set RSVP for every attendee else if ($method == 'REQUEST') { foreach ($event['attendees'] as $i => $attendee) { if (($rsvp || !isset($attendee['rsvp'])) && ($attendee['status'] != 'DELEGATED' && $attendee['role'] != 'NON-PARTICIPANT')) { $event['attendees'][$i]['rsvp']= (bool)$rsvp; } } } else if ($method == 'CANCEL') { if ($event['recurrence']) { unset($event['recurrence']['EXCEPTIONS']); } } // compose multipart message using PEAR:Mail_Mime $message = new Mail_mime("\r\n"); $message->setParam('text_encoding', 'quoted-printable'); $message->setParam('head_encoding', 'quoted-printable'); $message->setParam('head_charset', RCUBE_CHARSET); $message->setParam('text_charset', RCUBE_CHARSET . ";\r\n format=flowed"); $message->setContentType('multipart/alternative'); // compose common headers array $headers = array( 'From' => $sender, 'Date' => $this->rc->user_date(), 'Message-ID' => $this->rc->gen_message_id(), 'X-Sender' => $from, ); if ($agent = $this->rc->config->get('useragent')) { $headers['User-Agent'] = $agent; } $message->headers($headers); // attach ics file for this event $ical = libcalendaring::get_ical(); $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false); $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics'; $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCUBE_CHARSET . "; method=" . $method); return $message; } /** * Forward the given iTip event as delegation to another person * * @param array Event object to delegate * @param mixed Delegatee as string or hash array with keys 'name' and 'mailto' * @param boolean The delegator's RSVP flag * @param array List with indexes of new/updated attendees * @return boolean True on success, False on failure */ public function delegate_to(&$event, $delegate, $rsvp = false, &$attendees = array()) { if (is_string($delegate)) { $delegates = rcube_mime::decode_address_list($delegate, 1, false); if (count($delegates) > 0) { $delegate = reset($delegates); } } $emails = $this->lib->get_user_emails(); $me = $this->rc->user->list_emails(true); // find/create the delegate attendee $delegate_attendee = array( 'email' => $delegate['mailto'], 'name' => $delegate['name'], 'role' => 'REQ-PARTICIPANT', ); $delegate_index = count($event['attendees']); foreach ($event['attendees'] as $i => $attendee) { // set myself the DELEGATED-TO parameter if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $event['attendees'][$i]['delegated-to'] = $delegate['mailto']; $event['attendees'][$i]['status'] = 'DELEGATED'; $event['attendees'][$i]['role'] = 'NON-PARTICIPANT'; $event['attendees'][$i]['rsvp'] = $rsvp; $me['email'] = $attendee['email']; $delegate_attendee['role'] = $attendee['role']; } // the disired delegatee is already listed as an attendee else if (stripos($delegate['mailto'], $attendee['email']) !== false && $attendee['role'] != 'ORGANIZER') { $delegate_attendee = $attendee; $delegate_index = $i; break; } // TODO: remove previous delegatee (i.e. attendee that has DELEGATED-FROM == $me) } // set/add delegate attendee with RSVP=TRUE and DELEGATED-FROM parameter $delegate_attendee['rsvp'] = true; $delegate_attendee['status'] = 'NEEDS-ACTION'; $delegate_attendee['delegated-from'] = $me['email']; $event['attendees'][$delegate_index] = $delegate_attendee; $attendees[] = $delegate_index; $this->set_sender_email($me['email']); return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto'); } /** * Handler for calendar/itip-status requests */ public function get_itip_status($event, $existing = null) { $action = $event['rsvp'] ? 'rsvp' : ''; $status = $event['fallback']; - $latest = false; - $html = ''; + $latest = $resheduled = false; + $html = ''; if (is_numeric($event['changed'])) $event['changed'] = new DateTime('@'.$event['changed']); // check if the given itip object matches the last state if ($existing) { $latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']); } // determine action for REQUEST if ($event['method'] == 'REQUEST') { $html = html::div('rsvp-status', $this->gettext('acceptinvitation')); if ($existing) { $rsvp = $event['rsvp']; $emails = $this->lib->get_user_emails(); foreach ($existing['attendees'] as $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $status = strtoupper($attendee['status']); break; } } + + // Detect re-sheduling + if (!$latest) { + // FIXME: This is probably to simplistic, or maybe we should just check + // attendee's RSVP flag in the new event? + $resheduled = $existing['start'] != $event['start'] || $existing['end'] > $event['end']; + } } else { $rsvp = $event['rsvp'] && $this->rc->config->get('calendar_allow_itip_uninvited', true); } $status_lc = strtolower($status); if ($status_lc == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) { $html = html::div('rsvp-status', $this->gettext('notanattendee')); $action = 'import'; } else if (in_array($status_lc, $this->rsvp_status)) { $status_text = $this->gettext(($latest ? 'youhave' : 'youhavepreviously') . $status_lc); if ($existing && ($existing['sequence'] > $event['sequence'] || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] > $event['changed']))) { $action = ''; // nothing to do here, outdated invitation if ($status_lc == 'needs-action') $status_text = $this->gettext('outdatedinvitation'); } else if (!$existing && !$rsvp) { $action = 'import'; } + else if ($resheduled) { + $action = 'rsvp'; + } else if ($status_lc != 'needs-action') { $action = !$latest ? 'update' : ''; } $html = html::div('rsvp-status ' . $status_lc, $status_text); } } // determine action for REPLY else if ($event['method'] == 'REPLY') { // check whether the sender already is an attendee if ($existing) { $action = $this->rc->config->get('calendar_allow_itip_uninvited', true) ? 'accept' : ''; $listed = false; foreach ($existing['attendees'] as $attendee) { if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) { $status_lc = strtolower($status); if (in_array($status_lc, $this->rsvp_status)) { $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array( 'name' => 'attendee' . $status_lc, 'vars' => array( 'delegatedto' => rcube::Q($event['delegated-to'] ?: ($attendee['delegated-to'] ?: '?')), ) ))); } $action = $attendee['status'] == $status || !$latest ? '' : 'update'; $listed = true; break; } } if (!$listed) { $html = html::div('rsvp-status', $this->gettext('itipnewattendee')); } } else { $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); $action = ''; } } else if ($event['method'] == 'CANCEL') { if (!$existing) { $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound')); $action = ''; } } return array( - 'uid' => $event['uid'], - 'id' => asciiwords($event['uid'], true), - 'existing' => $existing ? true : false, - 'saved' => $existing ? true : false, - 'latest' => $latest, - 'status' => $status, - 'action' => $action, - 'html' => $html, + 'uid' => $event['uid'], + 'id' => asciiwords($event['uid'], true), + 'existing' => $existing ? true : false, + 'saved' => $existing ? true : false, + 'latest' => $latest, + 'status' => $status, + 'action' => $action, + 'resheduled' => $resheduled, + 'html' => $html, ); } /** * Build inline UI elements for iTip messages */ public function mail_itip_inline_ui($event, $method, $mime_id, $task, $message_date = null, $preview_url = null) { $buttons = array(); $dom_id = asciiwords($event['uid'], true); $rsvp_status = 'unknown'; // pass some metadata about the event and trigger the asynchronous status check $changed = is_object($event['changed']) ? $event['changed'] : $message_date; $metadata = array( 'uid' => $event['uid'], '_instance' => $event['_instance'], 'changed' => $changed ? $changed->format('U') : 0, 'sequence' => intval($event['sequence']), 'method' => $method, 'task' => $task, ); // create buttons to be activated from async request checking existence of this event in local calendars $buttons[] = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading')); // on iTip REPLY we have two options: if ($method == 'REPLY') { $title = $this->gettext('itipreply'); foreach ($event['attendees'] as $attendee) { if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER') { if (empty($event['_sender']) || self::compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) { $metadata['attendee'] = $attendee['email']; $rsvp_status = strtoupper($attendee['status']); if ($attendee['delegated-to']) { $metadata['delegated-to'] = $attendee['delegated-to']; } break; } } } // 1. update the attendee status on our copy $update_button = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')", 'value' => $this->gettext('updateattendeestatus'), )); // 2. accept or decline a new or delegate attendee $accept_buttons = html::tag('input', array( 'type' => 'button', 'class' => "button accept", 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')", 'value' => $this->gettext('acceptattendee'), )); $accept_buttons .= html::tag('input', array( 'type' => 'button', 'class' => "button decline", 'onclick' => "rcube_libcalendaring.decline_attendee_reply('" . rcube::JQ($mime_id) . "', '$task')", 'value' => $this->gettext('declineattendee'), )); $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button); $buttons[] = html::div(array('id' => 'accept-'.$dom_id, 'style' => 'display:none'), $accept_buttons); } // when receiving iTip REQUEST messages: else if ($method == 'REQUEST') { $emails = $this->lib->get_user_emails(); $title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation'); $metadata['rsvp'] = true; $metadata['sensitivity'] = $event['sensitivity']; if (is_object($event['start'])) { $metadata['date'] = $event['start']->format('U'); } // check for X-KOLAB-INVITATIONTYPE property and only show accept/decline buttons if (self::get_custom_property($event, 'X-KOLAB-INVITATIONTYPE') == 'CONFIRMATION') { $this->rsvp_actions = array('accepted','declined'); $metadata['nosave'] = true; } // 1. display RSVP buttons (if the user was invited) foreach ($this->rsvp_actions as $method) { $rsvp_buttons .= html::tag('input', array( 'type' => 'button', 'class' => "button $method", 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task', '$method', '$dom_id')", 'value' => $this->gettext('itip' . $method), )); } // add button to open calendar/preview if (!empty($preview_url)) { $msgref = $this->lib->ical_message->folder . '/' . $this->lib->ical_message->uid . '#' . $mime_id; $rsvp_buttons .= html::tag('input', array( 'type' => 'button', 'class' => "button preview", 'onclick' => "rcube_libcalendaring.open_itip_preview('" . rcube::JQ($preview_url) . "', '" . rcube::JQ($msgref) . "')", 'value' => $this->gettext('openpreview'), )); } // 2. update the local copy with minor changes $update_button = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')", 'value' => $this->gettext('updatemycopy'), )); // 3. Simply import the event without replying $import_button = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')", 'value' => $this->gettext('importtocalendar'), )); // check my status foreach ($event['attendees'] as $attendee) { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { $metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT'; $rsvp_status = !empty($attendee['status']) ? strtoupper($attendee['status']) : 'NEEDS-ACTION'; break; } } // add itip reply message controls $rsvp_buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($dom_id, $metadata['nosave'])); $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons); $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button); // prepare autocompletion for delegation dialog if (in_array('delegated', $this->rsvp_actions)) { $this->rc->autocomplete_init(); } } // for CANCEL messages, we can: else if ($method == 'CANCEL') { $title = $this->gettext('itipcancellation'); $event_prop = array_filter(array( 'uid' => $event['uid'], '_instance' => $event['_instance'], '_savemode' => $event['_savemode'], )); // 1. remove the event from our calendar $button_remove = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.remove_from_itip(" . rcube_output::json_serialize($event_prop) . ", '$task', '" . rcube::JQ($event['title']) . "')", 'value' => $this->gettext('removefromcalendar'), )); // 2. update our copy with status=cancelled $button_update = html::tag('input', array( 'type' => 'button', 'class' => 'button', 'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')", 'value' => $this->gettext('updatemycopy'), )); $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove . $button_update); $rsvp_status = 'CANCELLED'; $metadata['rsvp'] = true; } // append generic import button if ($import_button) { $buttons[] = html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button); } // pass some metadata about the event and trigger the asynchronous status check $metadata['fallback'] = $rsvp_status; $metadata['rsvp'] = intval($metadata['rsvp']); $this->rc->output->add_script("rcube_libcalendaring.fetch_itip_object_status(" . rcube_output::json_serialize($metadata) . ")", 'docready'); // get localized texts from the right domain foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee', 'cancel','itipdelegated','declineattendeeconfirm','itipcomment','delegateinvitation', 'delegateto','delegatersvpme','delegateinvalidaddress') as $label) { $this->rc->output->command('add_label', "itip.$label", $this->gettext($label)); } // show event details with buttons return $this->itip_object_details_table($event, $title) . html::div(array('class' => 'itip-buttons', 'id' => 'itip-buttons-' . asciiwords($metadata['uid'], true)), join('', $buttons)); } /** * Render an RSVP UI widget with buttons to respond on iTip invitations */ function itip_rsvp_buttons($attrib = array(), $actions = null) { $attrib += array('type' => 'button'); if (!$actions) $actions = $this->rsvp_actions; foreach ($actions as $method) { $buttons .= html::tag('input', array( 'type' => $attrib['type'], 'name' => $attrib['iname'], 'class' => 'button', 'rel' => $method, 'value' => $this->gettext('itip' . $method), )); } // add localized texts for the delegation dialog if (in_array('delegated', $actions)) { foreach (array('itipdelegated','itipcomment','delegateinvitation', 'delegateto','delegatersvpme','delegateinvalidaddress','cancel') as $label) { $this->rc->output->command('add_label', "itip.$label", $this->gettext($label)); } } foreach (array('all','current','future') as $mode) { $this->rc->output->command('add_label', "rsvpmode$mode", $this->gettext("rsvpmode$mode")); } $savemode_radio = new html_radiobutton(array('name' => '_rsvpmode', 'class' => 'rsvp-replymode')); return html::div($attrib, html::div('label', $this->gettext('acceptinvitation')) . html::div('rsvp-buttons', $buttons . html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id'])) ) ); } /** * Render UI elements to control iTip reply message sending */ public function itip_rsvp_options_ui($dom_id, $disable = false) { $itip_sending = $this->rc->config->get('calendar_itip_send_option', 3); // itip sending is entirely disabled if ($itip_sending === 0) { return ''; } // add checkbox to suppress itip reply message else if ($itip_sending >= 2) { $rsvp_additions = html::label(array('class' => 'noreply-toggle'), html::tag('input', array('type' => 'checkbox', 'id' => 'noreply-'.$dom_id, 'value' => 1, 'disabled' => $disable, 'checked' => ($itip_sending & 1) == 0)) . ' ' . $this->gettext('itipsuppressreply') ); } // add input field for reply comment $toggle_attrib = array( 'href' => '#toggle', 'class' => 'reply-comment-toggle', 'onclick' => '$(this).hide().parent().find(\'textarea\').show().focus()' ); $textarea_attrib = array( 'id' => 'reply-comment-' . $dom_id, 'name' => '_comment', 'cols' => 40, 'rows' => 6, 'style' => 'display:none', 'placeholder' => $this->gettext('itipcomment') ); $rsvp_additions .= html::a($toggle_attrib, $this->gettext('itipeditresponse')) . html::div('itip-reply-comment', html::tag('textarea', $textarea_attrib, '')); return $rsvp_additions; } /** * Render event/task details in a table */ function itip_object_details_table($event, $title) { $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails')); $table->add('ititle', $title); $table->add('title', rcube::Q($event['title'])); if ($event['start'] && $event['end']) { $table->add('label', $this->gettext('date')); $table->add('date', rcube::Q($this->lib->event_date_text($event))); } else if ($event['due'] && $event['_type'] == 'task') { $table->add('label', $this->gettext('date')); $table->add('date', rcube::Q($this->lib->event_date_text($event))); } if (!empty($event['recurrence_date'])) { $table->add('label', ''); $table->add('recurrence-id', $this->gettext($event['thisandfuture'] ? 'itipfutureoccurrence' : 'itipsingleoccurrence')); } else if (!empty($event['recurrence'])) { $table->add('label', $this->gettext('recurring')); $table->add('recurrence', $this->lib->recurrence_text($event['recurrence'])); } if ($event['location']) { $table->add('label', $this->gettext('location')); $table->add('location', rcube::Q($event['location'])); } if ($event['sensitivity'] && $event['sensitivity'] != 'public') { $table->add('label', $this->gettext('sensitivity')); $table->add('sensitivity', ucfirst($this->gettext($event['sensitivity'])) . '!'); } if ($event['status'] == 'COMPLETED' || $event['status'] == 'CANCELLED') { $table->add('label', $this->gettext('status')); $table->add('status', $this->gettext('status-' . strtolower($event['status']))); } if ($event['comment']) { $table->add('label', $this->gettext('comment')); $table->add('location', rcube::Q($event['comment'])); } return $table->show(); } /** * Create iTIP invitation token for later replies via URL * * @param array Hash array with event properties * @param string Attendee email address * @return string Invitation token */ public function store_invitation($event, $attendee) { // empty stub return false; } /** * Mark invitations for the given event as cancelled * * @param array Hash array with event properties */ public function cancel_itip_invitation($event) { // empty stub return false; } /** * Utility function to get the value of a custom property */ public static function get_custom_property($event, $name) { $ret = false; if (is_array($event['x-custom'])) { array_walk($event['x-custom'], function($prop, $i) use ($name, &$ret) { if (strcasecmp($prop[0], $name) === 0) { $ret = $prop[1]; } }); } return $ret; } /** * Compare email address */ public static function compare_email($value, $email, $email_utf = null) { $v1 = !empty($email) && strcasecmp($value, $email) === 0; $v2 = !empty($email_utf) && strcasecmp($value, $email_utf) === 0; return $v1 || $v2; } } diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js index 985271e1..c15fa048 100644 --- a/plugins/libcalendaring/libcalendaring.js +++ b/plugins/libcalendaring/libcalendaring.js @@ -1,1396 +1,1400 @@ /** * Basic Javascript utilities for calendar-related plugins * * @author Thomas Bruederli * * @licstart The following is the entire license notice for the * JavaScript code in this page. * * Copyright (C) 2012-2015, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * @licend The above is the entire license notice * for the JavaScript code in this page. */ function rcube_libcalendaring(settings) { // member vars this.settings = settings || {}; this.alarm_ids = []; this.alarm_dialog = null; this.snooze_popup = null; this.dismiss_link = null; this.group2expand = {}; // abort if env isn't set if (!settings || !settings.date_format) return; // private vars var me = this; var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0); var client_timezone = new Date().getTimezoneOffset(); // general datepicker settings var datepicker_settings = { // translate from fullcalendar 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 }; /** * Quote html entities */ var Q = this.quote_html = function(str) { return String(str).replace(//g, '>').replace(/"/g, '"'); }; /** * Create a nice human-readable string for the date/time range */ this.event_date_text = function(event, voice) { if (!event.start) return ''; if (!event.end) event.end = event.start; var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000, until = voice ? ' ' + rcmail.gettext('until','libcalendaring') + ' ' : ' — '; if (event.allDay) { fromto = this.format_datetime(event.start, 1, voice) + (duration > 86400 || event.start.getDay() != event.end.getDay() ? until + this.format_datetime(event.end, 1, voice) : ''); } else if (duration < 86400 && event.start.getDay() == event.end.getDay()) { fromto = this.format_datetime(event.start, 0, voice) + (duration > 0 ? until + this.format_datetime(event.end, 2, voice) : ''); } else { fromto = this.format_datetime(event.start, 0, voice) + (duration > 0 ? until + this.format_datetime(event.end, 0, voice) : ''); } return fromto; }; /** * From time and date strings to a real date object */ this.parse_datetime = function(time, date) { // we use the utility function from datepicker to parse dates var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date(); var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/); if (!isNaN(time_arr[0])) { date.setHours(time_arr[0]); if (time.match(/p[.m]*/i) && date.getHours() < 12) date.setHours(parseInt(time_arr[0]) + 12); else if (time.match(/a[.m]*/i) && date.getHours() == 12) date.setHours(0); } if (!isNaN(time_arr[1])) date.setMinutes(time_arr[1]); return date; } /** * Convert an ISO 8601 formatted date string from the server into a Date object. * Timezone information will be ignored, the server already provides dates in user's timezone. */ this.parseISO8601 = function(s) { // already a Date object? if (s && s.getMonth) { return s; } // force d to be on check's YMD, for daylight savings purposes var fixDate = function(d, check) { if (+d) { // prevent infinite looping on invalid dates while (d.getDate() != check.getDate()) { d.setTime(+d + (d < check ? 1 : -1) * 3600000); } } } // derived from http://delete.me.uk/2005/03/iso8601.html var m = s && s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/); if (!m) { return null; } var date = new Date(m[1], 0, 2), check = new Date(m[1], 0, 2, 9, 0); if (m[3]) { date.setMonth(m[3] - 1); check.setMonth(m[3] - 1); } if (m[5]) { date.setDate(m[5]); check.setDate(m[5]); } fixDate(date, check); if (m[7]) { date.setHours(m[7]); } if (m[8]) { date.setMinutes(m[8]); } if (m[10]) { date.setSeconds(m[10]); } if (m[12]) { date.setMilliseconds(Number("0." + m[12]) * 1000); } fixDate(date, check); return date; } /** * Turn the given date into an ISO 8601 date string understandable by PHPs strtotime() */ this.date2ISO8601 = function(date) { var zeropad = function(num) { return (num < 10 ? '0' : '') + num; }; return date.getFullYear() + '-' + zeropad(date.getMonth()+1) + '-' + zeropad(date.getDate()) + 'T' + zeropad(date.getHours()) + ':' + zeropad(date.getMinutes()) + ':' + zeropad(date.getSeconds()); }; /** * Format the given date object according to user's prefs */ this.format_datetime = function(date, mode, voice) { var res = ''; if (!mode || mode == 1) { res += $.datepicker.formatDate(voice ? 'MM d yy' : datepicker_settings.dateFormat, date, datepicker_settings); } if (!mode) { res += voice ? ' ' + rcmail.gettext('at','libcalendaring') + ' ' : ' '; } if (!mode || mode == 2) { res += this.format_time(date, voice); } return res; } /** * Clone from fullcalendar.js */ this.format_time = function(date, voice) { var zeroPad = function(n) { return (n < 10 ? '0' : '') + n; } var formatters = { s : function(d) { return d.getSeconds() }, ss : function(d) { return zeroPad(d.getSeconds()) }, m : function(d) { return d.getMinutes() }, mm : function(d) { return zeroPad(d.getMinutes()) }, h : function(d) { return d.getHours() % 12 || 12 }, hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, H : function(d) { return d.getHours() }, HH : function(d) { return zeroPad(d.getHours()) }, t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' } }; var i, i2, c, formatter, res = '', format = voice ? settings['time_format'].replace(':',' ').replace('HH','H').replace('hh','h').replace('mm','m').replace('ss','s') : settings['time_format']; for (i=0; i < format.length; i++) { c = format.charAt(i); for (i2=Math.min(i+2, format.length); i2 > i; i2--) { if (formatter = formatters[format.substring(i, i2)]) { res += formatter(date); i = i2 - 1; break; } } if (i2 == i) { res += c; } } return res; } /** * Convert the given Date object into a unix timestamp respecting browser's and user's timezone settings */ this.date2unixtime = function(date) { var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; // adjust DST offset return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset); } /** * Turn a unix timestamp value into a Date object */ this.fromunixtime = function(ts) { ts -= gmt_offset * 3600; var date = new Date(ts * 1000), dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; if (dst_offset) // adjust DST offset date.setTime((ts + 3600) * 1000); return date; } /** * Simple plaintext to HTML converter, makig URLs clickable */ this.text2html = function(str, maxlen, maxlines) { var html = Q(String(str)); // limit visible text length if (maxlen) { var morelink = '... '+rcmail.gettext('showmore','libcalendaring')+'', lines = html.split(/\r?\n/), words, out = '', len = 0; for (var i=0; i < lines.length; i++) { len += lines[i].length; if (maxlines && i == maxlines - 1) { out += lines[i] + '\n' + morelink; maxlen = html.length * 2; } else if (len > maxlen) { len = out.length; words = lines[i].split(' '); for (var j=0; j < words.length; j++) { len += words[j].length + 1; out += words[j] + ' '; if (len > maxlen) { out += morelink; maxlen = html.length * 2; maxlines = 0; } } out += '\n'; } else out += lines[i] + '\n'; } if (maxlen > str.length) out += ''; html = out; } // simple link parser (similar to rcube_string_replacer class in PHP) var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})'; var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-'; var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)', 'ig'); var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig'); var link_replace = function(matches, p1, p2) { var title = '', text = p2; if (p2 && p2.length > 55) { text = p2.substr(0, 45) + '...' + p2.substr(-8); title = p1 + p2; } return ''+p1+text+'' }; return html .replace(link_pattern, link_replace) .replace(mailto_pattern, '$1') .replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"') .replace(/\n/g, "
"); }; this.init_alarms_edit = function(prefix, index) { var edit_type = $(prefix+' select.edit-alarm-type'), dom_id = edit_type.attr('id'); // register events on alarm fields edit_type.change(function(){ $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')](); }); $(prefix+' select.edit-alarm-offset').change(function(){ var val = $(this).val(), parent = $(this).parent(); parent.find('.edit-alarm-date, .edit-alarm-time')[val == '@' ? 'show' : 'hide'](); parent.find('.edit-alarm-value').prop('disabled', val === '@' || val === '0'); parent.find('.edit-alarm-related')[val == '@' ? 'hide' : 'show'](); }); $(prefix+' .edit-alarm-date').removeClass('hasDatepicker').removeAttr('id').datepicker(datepicker_settings); $(prefix).on('click', 'a.delete-alarm', function(e){ if ($(this).closest('.edit-alarm-item').siblings().length > 0) { $(this).closest('.edit-alarm-item').remove(); } return false; }); // set a unique id attribute and set label reference accordingly if ((index || 0) > 0 && dom_id) { dom_id += ':' + (new Date().getTime()); edit_type.attr('id', dom_id); $(prefix+' label:first').attr('for', dom_id); } $(prefix).on('click', 'a.add-alarm', function(e){ var i = $(this).closest('.edit-alarm-item').siblings().length + 1; var item = $(this).closest('.edit-alarm-item').clone(false) .removeClass('first') .appendTo(prefix); me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i); $('select.edit-alarm-type, select.edit-alarm-offset', item).change(); return false; }); } this.set_alarms_edit = function(prefix, valarms) { $(prefix + ' .edit-alarm-item:gt(0)').remove(); var i, alarm, domnode, val, offset; for (i=0; i < valarms.length; i++) { alarm = valarms[i]; if (!alarm.action) alarm.action = 'DISPLAY'; if (i == 0) { domnode = $(prefix + ' .edit-alarm-item').eq(0); } else { domnode = $(prefix + ' .edit-alarm-item').eq(0).clone(false).removeClass('first').appendTo(prefix); this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i); } $('select.edit-alarm-type', domnode).val(alarm.action); $('select.edit-alarm-related', domnode).val(/END/i.test(alarm.related) ? 'end' : 'start'); if (String(alarm.trigger).match(/@(\d+)/)) { var ondate = this.fromunixtime(parseInt(RegExp.$1)); $('select.edit-alarm-offset', domnode).val('@'); $('input.edit-alarm-value', domnode).val(''); $('input.edit-alarm-date', domnode).val(this.format_datetime(ondate, 1)); $('input.edit-alarm-time', domnode).val(this.format_datetime(ondate, 2)); } else if (String(alarm.trigger).match(/^[-+]*0[MHDS]$/)) { $('input.edit-alarm-value', domnode).val('0'); $('select.edit-alarm-offset', domnode).val('0'); } else if (String(alarm.trigger).match(/([-+])(\d+)([MHDS])/)) { val = RegExp.$2; offset = ''+RegExp.$1+RegExp.$3; $('input.edit-alarm-value', domnode).val(val); $('select.edit-alarm-offset', domnode).val(offset); } } // set correct visibility by triggering onchange handlers $(prefix + ' select.edit-alarm-type, ' + prefix + ' select.edit-alarm-offset').change(); }; this.serialize_alarms = function(prefix) { var valarms = []; $(prefix + ' .edit-alarm-item').each(function(i, elem) { var val, offset, alarm = { action: $('select.edit-alarm-type', elem).val(), related: $('select.edit-alarm-related', elem).val() }; if (alarm.action) { offset = $('select.edit-alarm-offset', elem).val(); if (offset == '@') { alarm.trigger = '@' + me.date2unixtime(me.parse_datetime($('input.edit-alarm-time', elem).val(), $('input.edit-alarm-date', elem).val())); } else if (offset === '0') { alarm.trigger = '0S'; } else if (!isNaN((val = parseInt($('input.edit-alarm-value', elem).val()))) && val >= 0) { alarm.trigger = offset[0] + val + offset[1]; } valarms.push(alarm); } }); return valarms; }; // format time string var time_autocomplete_format = function(hour, minutes, start) { var time, diff, unit, duration = '', d = new Date(); d.setHours(hour); d.setMinutes(minutes); time = me.format_time(d); if (start) { diff = Math.floor((d.getTime() - start.getTime()) / 60000); if (diff > 0) { unit = 'm'; if (diff >= 60) { unit = 'h'; diff = Math.round(diff / 3) / 20; } duration = ' (' + diff + unit + ')'; } } return [time, duration]; }; var time_autocomplete_list = function(p, callback) { // Time completions var st, h, step = 15, result = [], now = new Date(), id = String(this.element.attr('id')), m = id.match(/^(.*)-(starttime|endtime)$/), start = (m && m[2] == 'endtime' && (st = $('#' + m[1] + '-starttime').val()) && $('#' + m[1] + '-startdate').val() == $('#' + m[1] + '-enddate').val()) ? me.parse_datetime(st, '') : null, full = p.term - 1 > 0 || p.term.length > 1, hours = start ? start.getHours() : (full ? me.parse_datetime(p.term, '') : now).getHours(), minutes = hours * 60 + (full ? 0 : now.getMinutes()), min = Math.ceil(minutes / step) * step % 60, hour = Math.floor(Math.ceil(minutes / step) * step / 60); // list hours from 0:00 till now for (h = start ? start.getHours() : 0; h < hours; h++) result.push(time_autocomplete_format(h, 0, start)); // list 15min steps for the next two hours for (; h < hour + 2 && h < 24; h++) { while (min < 60) { result.push(time_autocomplete_format(h, min, start)); min += step; } min = 0; } // list the remaining hours till 23:00 while (h < 24) result.push(time_autocomplete_format((h++), 0, start)); return callback(result); }; var time_autocomplete_open = function(event, ui) { // scroll to current time var $this = $(this), widget = $this.autocomplete('widget') menu = $this.data('ui-autocomplete').menu, amregex = /^(.+)(a[.m]*)/i, pmregex = /^(.+)(a[.m]*)/i, val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1'); widget.css('width', '10em'); if (val === '') menu._scrollIntoView(widget.children('li:first')); else widget.children().each(function() { var li = $(this), html = li.children().first().html() .replace(/\s+\(.+\)$/, '') .replace(amregex, '0:$1') .replace(pmregex, '1:$1'); if (html.indexOf(val) == 0) menu._scrollIntoView(li); }); }; /** * Initializes time autocompletion */ this.init_time_autocomplete = function(elem, props) { var default_props = { delay: 100, minLength: 1, appendTo: props.container, source: time_autocomplete_list, open: time_autocomplete_open, // change: time_autocomplete_change, select: function(event, ui) { $(this).val(ui.item[0]).change(); return false; } }; $(elem).attr('autocomplete', "off") .autocomplete($.extend(default_props, props)) .click(function() { // show drop-down upon clicks $(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " "); }); $(elem).data('ui-autocomplete')._renderItem = function(ul, item) { return $('
  • ') .data('ui-autocomplete-item', item) .append('' + item[0] + item[1] + '') .appendTo(ul); }; }; /***** Alarms handling *****/ /** * Display a notification for the given pending alarms */ this.display_alarms = function(alarms) { // clear old alert first if (this.alarm_dialog) this.alarm_dialog.dialog('destroy').remove(); var i, actions, adismiss, asnooze, alarm, html, audio_alarms = [], records = [], event_ids = [], buttons = {}; for (i=0; i < alarms.length; i++) { alarm = alarms[i]; alarm.start = this.parseISO8601(alarm.start); alarm.end = this.parseISO8601(alarm.end); if (alarm.action == 'AUDIO') { audio_alarms.push(alarm); continue; } event_ids.push(alarm.id); html = '

    ' + Q(alarm.title) + '

    '; html += '
    ' + Q(alarm.location || '') + '
    '; html += '
    ' + Q(this.event_date_text(alarm)) + '
    '; adismiss = $('').html(rcmail.gettext('dismiss','libcalendaring')).click(function(e){ me.dismiss_link = $(this); me.dismiss_alarm(me.dismiss_link.data('id'), 0, e); }); asnooze = $('').html(rcmail.gettext('snooze','libcalendaring')).click(function(e){ me.snooze_dropdown($(this), e); e.stopPropagation(); return false; }); actions = $('
    ').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id)); records.push($('
    ').addClass('alarm-item').html(html).append(actions)); } if (audio_alarms.length) this.audio_alarms(audio_alarms); if (!records.length) return; this.alarm_dialog = $('
    ').attr('id', 'alarm-display').append(records); buttons[rcmail.gettext('close')] = function() { $(this).dialog('close'); }; buttons[rcmail.gettext('dismissall','libcalendaring')] = function(e) { // submit dismissed event_ids to server me.dismiss_alarm(me.alarm_ids.join(','), 0, e); $(this).dialog('close'); }; this.alarm_dialog.appendTo(document.body).dialog({ modal: false, resizable: true, closeOnEscape: false, dialogClass: 'alarms', title: rcmail.gettext('alarmtitle','libcalendaring'), buttons: buttons, open: function() { setTimeout(function() { me.alarm_dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus(); }, 5); }, close: function() { $('#alarm-snooze-dropdown').hide(); $(this).dialog('destroy').remove(); me.alarm_dialog = null; me.alarm_ids = null; }, drag: function(event, ui) { $('#alarm-snooze-dropdown').hide(); } }); this.alarm_dialog.closest('div[role=dialog]').attr('role', 'alertdialog'); this.alarm_ids = event_ids; }; /** * Display a notification and play a sound for a set of alarms */ this.audio_alarms = function(alarms) { var elem, txt = [], src = rcmail.assets_path('plugins/libcalendaring/alarm'), plugin = navigator.mimeTypes ? navigator.mimeTypes['audio/mp3'] : {}; // first generate and display notification text $.each(alarms, function() { txt.push(this.title); }); rcmail.display_message(rcmail.gettext('alarmtitle','libcalendaring') + ': ' + Q(txt.join(', ')), 'notice', 10000); // Internet Explorer does not support wav files, // support in other browsers depends on enabled plugins, // so we use wav as a fallback src += bw.ie || (plugin && plugin.enabledPlugin) ? '.mp3' : '.wav'; // HTML5 try { elem = $('
  • ') .attr('data-value', this.date2ISO8601(date)) .html('' + Q(this.format_datetime(date, 1)) + '') .appendTo('#edit-recurrence-rdates'); $('').attr('href', '#del') .addClass('iconbutton delete') .html(rcmail.get_label('delete', 'libcalendaring')) .attr('title', rcmail.get_label('delete', 'libcalendaring')) .appendTo(li); }; // re-sort the list items by their 'data-value' attribute this.sort_rdates = function() { var mylist = $('#edit-recurrence-rdates'), listitems = mylist.children('li').get(); listitems.sort(function(a, b) { var compA = $(a).attr('data-value'); var compB = $(b).attr('data-value'); return (compA < compB) ? -1 : (compA > compB) ? 1 : 0; }) $.each(listitems, function(idx, item) { mylist.append(item); }); }; /***** Attendee form handling *****/ // expand the given contact group into individual event/task attendees this.expand_attendee_group = function(e, add, remove) { var id = (e.data ? e.data.email : null) || $(e.target).attr('data-email'), role_select = $(e.target).closest('tr').find('select.edit-attendee-role option:selected'); this.group2expand[id] = { link: e.target, data: $.extend({}, e.data || {}), adder: add, remover: remove } // copy group role from the according form element if (role_select.length) { this.group2expand[id].data.role = role_select.val(); } // register callback handler if (!this._expand_attendee_listener) { this._expand_attendee_listener = this.expand_attendee_callback; rcmail.addEventListener('plugin.expand_attendee_callback', function(result) { me._expand_attendee_listener(result); }); } rcmail.http_post('libcal/plugin.expand_attendee_group', { id: id, data: e.data || {} }, rcmail.set_busy(true, 'loading')); }; // callback from server to expand an attendee group this.expand_attendee_callback = function(result) { var attendee, id = result.id, data = this.group2expand[id], row = $(data.link).closest('tr'); // replace group entry with all members returned by the server if (data && data.adder && result.members && result.members.length) { for (var i=0; i < result.members.length; i++) { attendee = result.members[i]; attendee.role = data.data.role; attendee.cutype = 'INDIVIDUAL'; attendee.status = 'NEEDS-ACTION'; data.adder(attendee, null, row); } if (data.remover) { data.remover(data.link, id) } else { row.remove(); } delete this.group2expand[id]; } else { rcmail.display_message(result.error || rcmail.gettext('expandattendeegroupnodata','libcalendaring'), 'error'); } }; // Render message reference links to the given container this.render_message_links = function(links, container, edit, plugin) { var ul = $('