summaryrefslogtreecommitdiff
path: root/calendar/lightning
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2022-03-26 20:18:05 -0500
committerMatt A. Tobin <email@mattatobin.com>2022-03-26 20:18:05 -0500
commitc3dc8a1f81c2148a64bc99a194da4c10614e9b95 (patch)
tree6915845b08018db4ee37f09a7a8ea9b4c17ebb27 /calendar/lightning
parentc0d30f29a0a1d418442c9dc05c83ac6ef2921d15 (diff)
downloadaura-central-c3dc8a1f81c2148a64bc99a194da4c10614e9b95.tar.gz
Manually re-add calendar
Diffstat (limited to 'calendar/lightning')
-rw-r--r--calendar/lightning/Makefile.in88
-rw-r--r--calendar/lightning/app.ini35
-rw-r--r--calendar/lightning/build/get-platform.py13
-rw-r--r--calendar/lightning/build/makeversion.py20
-rw-r--r--calendar/lightning/build/universal.mk45
-rw-r--r--calendar/lightning/chrome.manifest0
-rw-r--r--calendar/lightning/components/calItipProtocolHandler.js137
-rw-r--r--calendar/lightning/components/calItipProtocolHandler.manifest8
-rw-r--r--calendar/lightning/components/lightningTextCalendarConverter.js92
-rw-r--r--calendar/lightning/components/lightningTextCalendarConverter.manifest3
-rw-r--r--calendar/lightning/components/moz.build12
-rw-r--r--calendar/lightning/content/html-item-editing/lightning-item-iframe.html28
-rw-r--r--calendar/lightning/content/html-item-editing/react-code.js572
-rw-r--r--calendar/lightning/content/imip-bar-overlay.xul266
-rw-r--r--calendar/lightning/content/imip-bar.js470
-rw-r--r--calendar/lightning/content/lightning-calendar-creation.js17
-rw-r--r--calendar/lightning/content/lightning-calendar-creation.xul30
-rw-r--r--calendar/lightning/content/lightning-calendar-properties.js17
-rw-r--r--calendar/lightning/content/lightning-calendar-properties.xul32
-rw-r--r--calendar/lightning/content/lightning-invitation.xhtml74
-rw-r--r--calendar/lightning/content/lightning-item-iframe.js4061
-rw-r--r--calendar/lightning/content/lightning-item-iframe.xul740
-rw-r--r--calendar/lightning/content/lightning-item-panel.js1096
-rw-r--r--calendar/lightning/content/lightning-item-panel.xul165
-rw-r--r--calendar/lightning/content/lightning-item-toolbar.xul181
-rw-r--r--calendar/lightning/content/lightning-menus.xul814
-rw-r--r--calendar/lightning/content/lightning-migration.xul39
-rw-r--r--calendar/lightning/content/lightning-toolbar.xul223
-rw-r--r--calendar/lightning/content/lightning-utils.js86
-rw-r--r--calendar/lightning/content/lightning-widgets.css11
-rw-r--r--calendar/lightning/content/lightning-widgets.xml25
-rw-r--r--calendar/lightning/content/lightning.js170
-rw-r--r--calendar/lightning/content/messenger-overlay-accountCentral.xul31
-rw-r--r--calendar/lightning/content/messenger-overlay-messageWindow.xul10
-rw-r--r--calendar/lightning/content/messenger-overlay-preferences.js27
-rw-r--r--calendar/lightning/content/messenger-overlay-preferences.xul67
-rw-r--r--calendar/lightning/content/messenger-overlay-sidebar.js946
-rw-r--r--calendar/lightning/content/messenger-overlay-sidebar.xul315
-rw-r--r--calendar/lightning/content/suite-overlay-addons.xul39
-rw-r--r--calendar/lightning/content/suite-overlay-preferences.xul68
-rw-r--r--calendar/lightning/content/suite-overlay-sidebar.js47
-rw-r--r--calendar/lightning/content/suite-overlay-sidebar.xul42
-rw-r--r--calendar/lightning/install.rdf33
-rw-r--r--calendar/lightning/jar.mn111
-rw-r--r--calendar/lightning/lightning-packager.mk196
-rw-r--r--calendar/lightning/lightning-tests.mk23
-rw-r--r--calendar/lightning/locales/Makefile.in11
-rw-r--r--calendar/lightning/locales/jar.mn11
-rw-r--r--calendar/lightning/locales/moz.build8
-rw-r--r--calendar/lightning/modules/ltnInvitationUtils.jsm588
-rw-r--r--calendar/lightning/modules/ltnUtils.jsm22
-rw-r--r--calendar/lightning/modules/moz.build9
-rw-r--r--calendar/lightning/moz.build53
-rw-r--r--calendar/lightning/themes/common/html-item-editing.css98
-rw-r--r--calendar/lightning/themes/common/images/mode-switch-icons.pngbin0 -> 6947 bytes
-rw-r--r--calendar/lightning/themes/common/imip.css117
-rw-r--r--calendar/lightning/themes/common/lightning.css47
-rw-r--r--calendar/lightning/themes/common/suite-accountCentral.css9
-rw-r--r--calendar/lightning/themes/linux/accountCentral.css11
-rw-r--r--calendar/lightning/themes/linux/imip.css5
-rw-r--r--calendar/lightning/themes/linux/lightning-toolbar.css130
-rw-r--r--calendar/lightning/themes/linux/lightning-widgets.css10
-rw-r--r--calendar/lightning/themes/linux/lightning.css248
-rw-r--r--calendar/lightning/themes/windows/accountCentral.css11
-rw-r--r--calendar/lightning/themes/windows/images/imip-aero.pngbin0 -> 1277 bytes
-rw-r--r--calendar/lightning/themes/windows/images/mode-switch-icons-aero.pngbin0 -> 2004 bytes
-rw-r--r--calendar/lightning/themes/windows/images/mode-switch-icons-inverted.pngbin0 -> 310 bytes
-rw-r--r--calendar/lightning/themes/windows/imip.css5
-rw-r--r--calendar/lightning/themes/windows/imip.pngbin0 -> 1842 bytes
-rw-r--r--calendar/lightning/themes/windows/lightning-toolbar.css432
-rw-r--r--calendar/lightning/themes/windows/lightning-widgets.css10
-rw-r--r--calendar/lightning/themes/windows/lightning.css420
-rw-r--r--calendar/lightning/versions.mk11
73 files changed, 13791 insertions, 0 deletions
diff --git a/calendar/lightning/Makefile.in b/calendar/lightning/Makefile.in
new file mode 100644
index 000000000..fb6d888ff
--- /dev/null
+++ b/calendar/lightning/Makefile.in
@@ -0,0 +1,88 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Calendar builders currently use STRIP_XPI to reduce the binary component in
+# Lightning.
+
+XPI_PKGNAME = lightning-$(LIGHTNING_VERSION).$(AB_CD).$(MOZ_PKG_PLATFORM)
+XPI_VERSION = $(LIGHTNING_VERSION)
+
+XPI_EM_ID = {e2fda1a4-762b-4020-b5ad-a41df1933103}
+
+ifneq (,$(findstring a,$(LIGHTNING_VERSION)))
+DEFINES += -DLIGHTNING_PRERELEASE_VERSION=1
+endif
+
+# Enable nightly updates on aurora and nightly channel
+ifeq (nightly,$(subst aurora,nightly,$(MOZ_UPDATE_CHANNEL)))
+DEFINES += -DLIGHTNING_UPDATE_LOCATION=https://calendar.mozilla.org/update.php
+endif
+
+# Gecko milestone
+GRE_MILESTONE = $(shell $(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py $(DIST)/bin/platform.ini Build Milestone)
+ifdef GRE_MILESTONE
+DEFINES += -DGRE_MILESTONE=$(GRE_MILESTONE)
+endif
+
+# comm-central source repo and stamp
+SOURCE_STAMP ?= $(firstword $(shell hg -R $(topsrcdir) parent --template='{node}\n' 2>/dev/null))
+ifdef SOURCE_STAMP
+DEFINES += -DSOURCE_STAMP='$(SOURCE_STAMP)'
+endif
+
+SOURCE_REPO := $(shell hg -R $(topsrcdir) showconfig paths.default 2>/dev/null | sed -e 's/^ssh:/http:/')
+ifdef SOURCE_REPO
+DEFINES += -DSOURCE_REPO='$(SOURCE_REPO)'
+endif
+
+# Mozilla source repo and stamps
+MOZ_SOURCE_STAMP = $(firstword $(shell hg -R $(MOZILLA_SRCDIR) parent --template='{node}\n' 2>/dev/null))
+ifdef MOZ_SOURCE_STAMP
+DEFINES += -DMOZ_SOURCE_STAMP='$(MOZ_SOURCE_STAMP)'
+endif
+
+MOZ_SOURCE_REPO := $(shell hg -R $(MOZILLA_SRCDIR) showconfig paths.default 2>/dev/null | sed -e 's/^ssh:/http:/')
+ifdef MOZ_SOURCE_REPO
+DEFINES += -DMOZ_SOURCE_REPO='$(MOZ_SOURCE_REPO)'
+endif
+
+# Install as a global extension in
+# dist/bin/extensions/
+XPI_INSTALL_EXTENSION = $(XPI_EM_ID)
+
+DEFINES += -DTHUNDERBIRD_VERSION=$(THUNDERBIRD_VERSION) \
+ -DTHUNDERBIRD_MAXVERSION=$(THUNDERBIRD_MAXVERSION) \
+ -DSEAMONKEY_VERSION=$(SEAMONKEY_VERSION) \
+ -DSEAMONKEY_MAXVERSION=$(SEAMONKEY_MAXVERSION) \
+ -DLIGHTNING_VERSION=$(LIGHTNING_VERSION) \
+ -DTARGET_PLATFORM=$(OS_TARGET)_$(TARGET_XPCOM_ABI) \
+ -DXPI_EM_ID=$(XPI_EM_ID) \
+ $(NULL)
+
+MOZ_BUILDID = $(shell $(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py $(DIST)/bin/application.ini App BuildID)
+DEFINES += -DMOZ_BUILDID=$(MOZ_BUILDID)
+
+include $(topsrcdir)/config/rules.mk
+include $(srcdir)/versions.mk
+include $(srcdir)/lightning-packager.mk
+include $(srcdir)/lightning-tests.mk
+
+# For Lightning, we also need to preprocess the l10n prefs. Pull in the en-US
+# copy if the files doesn't exist.
+repack-process-extrafiles: lightning-extrafiles
+lightning-extrafiles: LTN_ABCD_L10NJS=$(call EXPAND_LOCALE_SRCDIR,calendar/locales)/lightning-l10n.js
+lightning-extrafiles: LTN_ANY_L10NJS=$(if $(wildcard $(LTN_ABCD_L10NJS)),$(LTN_ABCD_L10NJS),$(topsrcdir)/calendar/locales/en-US/lightning-l10n.js)
+lightning-extrafiles:
+ $(call py_action,preprocessor,$(PREF_PPFLAGS) $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $(LTN_ANY_L10NJS) -o $(DIST)/$(UNIVERSAL_PATH)xpi-stage/$(L10N_XPI_NAME)/$(PREF_DIR)/lightning-l10n.js)
+
+ident:
+ @printf 'comm_revision '
+ @$(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py \
+ $(FINAL_TARGET)/app.ini App SourceStamp
+ @printf 'moz_revision '
+ @$(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py \
+ $(FINAL_TARGET)/app.ini Build SourceStamp
+ @printf 'buildid '
+ @$(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py \
+ $(FINAL_TARGET)/app.ini App BuildID
diff --git a/calendar/lightning/app.ini b/calendar/lightning/app.ini
new file mode 100644
index 000000000..47c4f6d1c
--- /dev/null
+++ b/calendar/lightning/app.ini
@@ -0,0 +1,35 @@
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0. If a copy of the MPL was not distributed with this
+; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+; This is a mock application.ini for Lightning. It allows easy access to
+; extension properties by the build system.
+
+#filter substitution
+[App]
+Vendor=Mozilla
+Name=Lightning
+Version=@LIGHTNING_VERSION@
+BuildID=@MOZ_BUILDID@
+
+#ifdef SOURCE_REPO
+SourceRepository=@SOURCE_REPO@
+#endif
+#ifdef SOURCE_STAMP
+SourceStamp=@SOURCE_STAMP@
+#endif
+
+Copyright=Copyright (c) 1998 - 2010 mozilla.org
+ID=@XPI_EM_ID@
+
+[Build]
+#ifdef MOZ_SOURCE_REPO
+SourceRepository=@MOZ_SOURCE_REPO@
+#endif
+#ifdef MOZ_SOURCE_STAMP
+SourceStamp=@MOZ_SOURCE_STAMP@
+#endif
+
+[Gecko]
+MinVersion=@GRE_MILESTONE@
+MaxVersion=@GRE_MILESTONE@
diff --git a/calendar/lightning/build/get-platform.py b/calendar/lightning/build/get-platform.py
new file mode 100644
index 000000000..3c0e4fa4f
--- /dev/null
+++ b/calendar/lightning/build/get-platform.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Get the target platform from a set an install.rdf file
+
+import sys
+from xml.dom.minidom import parse
+
+doc = parse(sys.argv[1] + "/install.rdf")
+elem = doc.getElementsByTagName("em:targetPlatform")[0]
+print elem.firstChild.nodeValue
diff --git a/calendar/lightning/build/makeversion.py b/calendar/lightning/build/makeversion.py
new file mode 100644
index 000000000..f6e30754c
--- /dev/null
+++ b/calendar/lightning/build/makeversion.py
@@ -0,0 +1,20 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys
+import re
+
+# Change this number to add an amount to the minor, i.e with MINOR_ADD=2,
+# 24.2.0 becomes 2.6.4 instead of 2.6.2
+MINOR_ADD=0
+
+def makeversion(x):
+ parts = x.split('.')
+ major = str((int(parts[0]) + 2))
+ parts[0] = major[:-1] + "." + major[-1]
+ if len(parts) > 1 and parts[1].isdigit():
+ parts[1] = str(int(parts[1]) + MINOR_ADD)
+ return re.sub(r'.0([ab][0-9]*|)$', r'\1', '.'.join(parts))
+
+print(makeversion(sys.argv[1]))
diff --git a/calendar/lightning/build/universal.mk b/calendar/lightning/build/universal.mk
new file mode 100644
index 000000000..baf98f317
--- /dev/null
+++ b/calendar/lightning/build/universal.mk
@@ -0,0 +1,45 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+ifndef OBJDIR
+OBJDIR_ARCH_1 = $(MOZ_OBJDIR)/$(firstword $(MOZ_BUILD_PROJECTS))
+OBJDIR_ARCH_2 = $(MOZ_OBJDIR)/$(word 2,$(MOZ_BUILD_PROJECTS))
+DIST_ARCH_1 = $(OBJDIR_ARCH_1)/dist
+DIST_ARCH_2 = $(OBJDIR_ARCH_2)/dist
+DIST_UNI = $(DIST_ARCH_1)/universal
+OBJDIR = $(OBJDIR_ARCH_1)
+endif
+
+topsrcdir = $(TOPSRCDIR)
+DEPTH = $(OBJDIR)
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/platform/system/installer/package-name.mk
+
+THUNDERBIRD_VERSION := $(shell cat $(topsrcdir)/mail/config/version.txt)
+LIGHTNING_VERSION := $(shell $(PYTHON) $(topsrcdir)/calendar/lightning/build/makeversion.py $(word 1,$(MOZ_PKG_VERSION) $(THUNDERBIRD_VERSION)))
+XPI_PKGNAME = lightning-$(LIGHTNING_VERSION).$(AB_CD).$(MOZ_PKG_PLATFORM)
+
+STANDALONE_MAKEFILE := 1
+include $(TOPSRCDIR)/config/config.mk
+
+define unify_lightning
+mkdir -p $(DIST_UNI)/$1
+rm -rf $(DIST_UNI)/$1/$2*
+cp -R $(DIST_ARCH_1)/$1/$2 $(DIST_UNI)/$1
+grep -v em:targetPlatform $(DIST_ARCH_1)/$1/$2/install.rdf > $(DIST_UNI)/$1/$2/install.rdf
+endef
+
+define unify_lightning_repackage
+$(call py_action,zip,-C $(DIST_UNI)/$1/$2 ../$(XPI_PKGNAME).xpi '*')
+endef
+
+postflight_all:
+ $(call unify_lightning,xpi-stage,lightning)
+ $(call unify_lightning_repackage,xpi-stage,lightning)
+ifdef NIGHTLY_BUILD
+ $(call unify_lightning,$(MOZ_APP_DISPLAYNAME).app/Contents/Resources/extensions,{e2fda1a4-762b-4020-b5ad-a41df1933103})
+else
+ $(call unify_lightning,$(MOZ_APP_DISPLAYNAME).app/Contents/Resources/distribution/extensions,{e2fda1a4-762b-4020-b5ad-a41df1933103})
+endif
diff --git a/calendar/lightning/chrome.manifest b/calendar/lightning/chrome.manifest
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/calendar/lightning/chrome.manifest
diff --git a/calendar/lightning/components/calItipProtocolHandler.js b/calendar/lightning/components/calItipProtocolHandler.js
new file mode 100644
index 000000000..7615ae9c7
--- /dev/null
+++ b/calendar/lightning/components/calItipProtocolHandler.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+var Ci = Components.interfaces;
+
+var ITIP_HANDLER_MIMETYPE = "application/x-itip-internal";
+var ITIP_HANDLER_PROTOCOL = "moz-cal-handle-itip";
+var NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001;
+
+
+function NYI() {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+}
+
+function ItipChannel(URI) {
+ this.URI = this.originalURI = URI;
+}
+var ItipChannelClassID = Components.ID("{643e0328-36f6-411d-a107-16238dff9cd7}");
+var ItipChannelInterfaces = [
+ Components.interfaces.nsIChannel,
+ Components.interfaces.nsIRequest
+];
+ItipChannel.prototype = {
+ classID: ItipChannelClassID,
+ QueryInterface: XPCOMUtils.generateQI(ItipChannelInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: ItipChannelClassID,
+ contractID: "@mozilla.org/calendar/itip-channel;1",
+ classDescription: "Calendar Itip Channel",
+ interfaces: ItipChannelInterfaces,
+ }),
+
+ contentType: ITIP_HANDLER_MIMETYPE,
+ loadAttributes: null,
+ contentLength: 0,
+ owner: null,
+ loadGroup: null,
+ notificationCallbacks: null,
+ securityInfo: null,
+
+ open: NYI,
+ asyncOpen: function(observer, ctxt) {
+ observer.onStartRequest(this, ctxt);
+ },
+ asyncRead: function(listener, ctxt) {
+ return listener.onStartRequest(this, ctxt);
+ },
+
+ isPending: function() { return true; },
+ status: Components.results.NS_OK,
+ cancel: function(status) { this.status = status; },
+ suspend: NYI,
+ resume: NYI,
+};
+
+function ItipProtocolHandler() {
+ this.wrappedJSObject = this;
+}
+var ItipProtocolHandlerClassID = Components.ID("{6e957006-b4ce-11d9-b053-001124736B74}");
+var ItipProtocolHandlerInterfaces = [Components.interfaces.nsIProtocolHandler];
+ItipProtocolHandler.prototype = {
+ classID: ItipProtocolHandlerClassID,
+ QueryInterface: XPCOMUtils.generateQI(ItipProtocolHandlerInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: ItipProtocolHandlerClassID,
+ contractID: "@mozilla.org/network/protocol;1?name=" + ITIP_HANDLER_PROTOCOL,
+ classDescription: "iTIP Protocol Handler",
+ interfaces: ItipProtocolHandlerInterfaces
+ }),
+
+ protocolFlags: Ci.nsIProtocolHandler.URI_NORELATIVE | Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD,
+ allowPort: () => false,
+ isSecure: false,
+ newURI: function(spec, charSet, baseURI) {
+ let cls = Components.classes["@mozilla.org/network/standard-url;1"];
+ let url = cls.createInstance(Ci.nsIStandardURL);
+ url.init(Ci.nsIStandardURL.URLTYPE_STANDARD, 0, spec, charSet, baseURI);
+ dump("Creating new URI for " + spec + "\n");
+ return url.QueryInterface(Ci.nsIURI);
+ },
+
+ newChannel: function(URI) {
+ return this.newChannel2(URI, null);
+ },
+
+ newChannel2: function(URI, aLoadInfo) {
+ dump("Creating new ItipChannel for " + URI + "\n");
+ return new ItipChannel(URI);
+ },
+};
+
+function ItipContentHandler() {
+ this.wrappedJSObject = this;
+}
+var ItipContentHandlerClassID = Components.ID("{47c31f2b-b4de-11d9-bfe6-001124736B74}");
+var ItipContentHandlerInterfaces = [Components.interfaces.nsIContentHandler];
+ItipContentHandler.prototype = {
+ classID: ItipContentHandlerClassID,
+ QueryInterface: XPCOMUtils.generateQI(ItipContentHandlerInterfaces),
+ classInfo: XPCOMUtils.generateCI({
+ classID: ItipContentHandlerClassID,
+ contractID: "@mozilla.org/uriloader/content-handler;1?type=" + ITIP_HANDLER_MIMETYPE,
+ classDescription: "Lightning text/calendar content handler",
+ interfaces: ItipContentHandlerInterfaces
+ }),
+
+ handleContent: function(contentType, windowTarget, request) {
+ let channel = request.QueryInterface(Ci.nsIChannel);
+ let uri = channel.URI.spec;
+ if (!uri.startsWith(ITIP_HANDLER_PROTOCOL + ":")) {
+ cal.ERROR("Unexpected iTIP uri: " + uri + "\n");
+ throw NS_ERROR_WONT_HANDLE_CONTENT;
+ }
+ // moz-cal-handle-itip:///?
+ let paramString = uri.substring(ITIP_HANDLER_PROTOCOL.length + 4);
+ let paramArray = paramString.split("&");
+ let paramBlock = { };
+ paramArray.forEach((value) => {
+ let parts = value.split("=");
+ paramBlock[parts[0]] = unescape(unescape(parts[1]));
+ });
+ // dump("content-handler: have params " + paramBlock.toSource() + "\n");
+ let event = cal.createEvent(paramBlock.data);
+ dump("Processing iTIP event '" + event.title + "' from " +
+ event.organizer.id + " (" + event.id + ")\n");
+ let calMgr = cal.getCalendarManager();
+ let cals = calMgr.getCalendars({});
+ cals[0].addItem(event, null);
+ }
+};
+
+var components = [ItipChannel, ItipProtocolHandler, ItipContentHandler];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/calendar/lightning/components/calItipProtocolHandler.manifest b/calendar/lightning/components/calItipProtocolHandler.manifest
new file mode 100644
index 000000000..561872da7
--- /dev/null
+++ b/calendar/lightning/components/calItipProtocolHandler.manifest
@@ -0,0 +1,8 @@
+component {643e0328-36f6-411d-a107-16238dff9cd7} calItipProtocolHandler.js
+contract @mozilla.org/calendar/itip-channel;1 {643e0328-36f6-411d-a107-16238dff9cd7}
+
+component {6e957006-b4ce-11d9-b053-001124736B74} calItipProtocolHandler.js
+contract @mozilla.org/network/protocol;1?name=moz-cal-handle-itip {6e957006-b4ce-11d9-b053-001124736B74}
+
+component {47c31f2b-b4de-11d9-bfe6-001124736B74} calItipProtocolHandler.js
+contract @mozilla.org/uriloader/content-handler;1?type=application/x-itip-internal {47c31f2b-b4de-11d9-bfe6-001124736B74}
diff --git a/calendar/lightning/components/lightningTextCalendarConverter.js b/calendar/lightning/components/lightningTextCalendarConverter.js
new file mode 100644
index 000000000..cf33ea6da
--- /dev/null
+++ b/calendar/lightning/components/lightningTextCalendarConverter.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calXMLUtils.jsm");
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
+Components.utils.import("resource://calendar/modules/ltnInvitationUtils.jsm");
+
+function ltnMimeConverter() {
+ this.wrappedJSObject = this;
+}
+
+var ltnMimeConverterClassID = Components.ID("{c70acb08-464e-4e55-899d-b2c84c5409fa}");
+var ltnMimeConverterInterfaces = [Components.interfaces.nsISimpleMimeConverter];
+ltnMimeConverter.prototype = {
+ classID: ltnMimeConverterClassID,
+ QueryInterface: XPCOMUtils.generateQI(ltnMimeConverterInterfaces),
+
+ classInfo: XPCOMUtils.generateCI({
+ classID: ltnMimeConverterClassID,
+ contractID: "@mozilla.org/lightning/mime-converter;1",
+ classDescription: "Lightning text/calendar handler",
+ interfaces: ltnMimeConverterInterfaces
+ }),
+
+ uri: null,
+
+ convertToHTML: function(contentType, data) {
+ let parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
+ .createInstance(Components.interfaces.calIIcsParser);
+ parser.parseString(data);
+ let event = null;
+ for (let item of parser.getItems({})) {
+ if (cal.isEvent(item)) {
+ if (item.hasProperty("X-MOZ-FAKED-MASTER")) {
+ // if it's a faked master, take any overridden item to get a real occurrence:
+ let exc = item.recurrenceInfo.getExceptionFor(item.startDate);
+ cal.ASSERT(exc, "unexpected!");
+ if (exc) {
+ item = exc;
+ }
+ }
+ event = item;
+ break;
+ }
+ }
+ if (!event) {
+ return "";
+ }
+
+ let itipItem = null;
+ let msgOverlay = "";
+ let msgWindow = null;
+
+ itipItem = Components.classes["@mozilla.org/calendar/itip-item;1"]
+ .createInstance(Components.interfaces.calIItipItem);
+ itipItem.init(data);
+
+ // this.uri is the message URL that we are processing.
+ // We use it to get the nsMsgHeaderSink to store the calItipItem.
+ if (this.uri) {
+ try {
+ let msgUrl = this.uri.QueryInterface(Components.interfaces.nsIMsgMailNewsUrl);
+ msgWindow = msgUrl.msgWindow;
+ itipItem.sender = msgUrl.mimeHeaders.extractHeader("From", false);
+ } catch (exc) {
+ // msgWindow is optional in some scenarios
+ // (e.g. gloda in action, throws NS_ERROR_INVALID_POINTER then)
+ }
+ }
+
+ // msgOverlay needs to be defined irrespectively of the existance of msgWindow to not break
+ // printing of invitation emails
+ let dom = ltn.invitation.createInvitationOverlay(event, itipItem);
+ msgOverlay = cal.xml.serializeDOM(dom);
+
+ if (msgWindow) {
+ let sinkProps = msgWindow.msgHeaderSink.properties;
+ sinkProps.setPropertyAsInterface("itipItem", itipItem);
+ sinkProps.setPropertyAsAUTF8String("msgOverlay", msgOverlay);
+
+ // Notify the observer that the itipItem is available
+ Services.obs.notifyObservers(null, "onItipItemCreation", 0);
+ }
+ return msgOverlay;
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ltnMimeConverter]);
diff --git a/calendar/lightning/components/lightningTextCalendarConverter.manifest b/calendar/lightning/components/lightningTextCalendarConverter.manifest
new file mode 100644
index 000000000..76057cfd7
--- /dev/null
+++ b/calendar/lightning/components/lightningTextCalendarConverter.manifest
@@ -0,0 +1,3 @@
+component {c70acb08-464e-4e55-899d-b2c84c5409fa} lightningTextCalendarConverter.js
+contract @mozilla.org/lightning/mime-converter;1 {c70acb08-464e-4e55-899d-b2c84c5409fa}
+category simple-mime-converters text/calendar @mozilla.org/lightning/mime-converter;1
diff --git a/calendar/lightning/components/moz.build b/calendar/lightning/components/moz.build
new file mode 100644
index 000000000..4d53c7aca
--- /dev/null
+++ b/calendar/lightning/components/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_COMPONENTS += [
+ 'calItipProtocolHandler.js',
+ 'calItipProtocolHandler.manifest',
+ 'lightningTextCalendarConverter.js',
+ 'lightningTextCalendarConverter.manifest',
+]
+
diff --git a/calendar/lightning/content/html-item-editing/lightning-item-iframe.html b/calendar/lightning/content/html-item-editing/lightning-item-iframe.html
new file mode 100644
index 000000000..4cc17774c
--- /dev/null
+++ b/calendar/lightning/content/html-item-editing/lightning-item-iframe.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Event in a Tab</title>
+ <!--
+ "chrome://global/skin/global.css" cannot be included here due to
+ namespaces, see https://bugzilla.mozilla.org/show_bug.cgi?id=1297392#c4
+ -->
+ <link rel="stylesheet" type="text/css" href="chrome://lightning-common/skin/html-item-editing.css">
+ </head>
+ <body>
+ <!-- This div's id must be "container" for React.js to use it -->
+ <div id='container'>
+ <p>
+ If you can see this, React is still loading or is not working right.
+ </p>
+ </div>
+ <script type="application/javascript" src="resource://devtools/client/shared/vendor/react.js"></script>
+ <script type="application/javascript" src="resource://devtools/client/shared/vendor/react-dom.js"></script>
+ <script src='chrome://calendar/content/calendar-dialog-utils.js'></script>
+ <script src='chrome://calendar/content/calendar-ui-utils.js'></script>
+ <script src='chrome://calendar/content/calUtils.js'></script>
+ <script src='chrome://global/content/globalOverlay.js'></script>
+ <script src='chrome://lightning/content/lightning-item-iframe.js'></script>
+ <script src='chrome://lightning/content/html-item-editing/react-code.js'></script>
+ </body>
+</html>
diff --git a/calendar/lightning/content/html-item-editing/react-code.js b/calendar/lightning/content/html-item-editing/react-code.js
new file mode 100644
index 000000000..0b0d07b5c
--- /dev/null
+++ b/calendar/lightning/content/html-item-editing/react-code.js
@@ -0,0 +1,572 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file contains code that uses react.js
+
+/* exported gTopComponent, DatePicker, TopComponent */
+
+var gTopComponent = null;
+
+var Tabstrip = React.createClass({
+ handleChange: function(index) {
+ // The click handler will update the state with
+ // the index of the focused menu entry
+ this.props.onInput(this.props.keyprop, index);
+ },
+ render: function() {
+ return React.DOM.ul(
+ { id: "tabstrip" },
+ this.props.tabs.map((tab, index) => {
+ let style = (this.props.activeTab == index) ? "activeTab" : "";
+ // The bind() method makes the index
+ // available to the handleChange function:
+ return React.DOM.li({
+ className: style + " tab",
+ key: "tabkey" + index,
+ onClick: this.handleChange.bind(this, index)
+ }, tab);
+ })
+ );
+ }
+});
+
+var TextField = React.createClass({
+ handleChange: function(event) {
+ this.props.onInput(this.props.keyprop, event.target.value);
+ },
+ render: function() {
+ return React.DOM.input({
+ type: "text",
+ // placeholder: "New Event"
+ value: this.props.value,
+ onChange: this.handleChange
+ });
+ }
+});
+
+var TextArea = React.createClass({
+ handleChange: function(event) {
+ this.props.onInput(this.props.keyprop, event.target.value);
+ },
+ render: function() {
+ return React.DOM.textarea({
+ type: "text",
+ // placeholder: "New Event"
+ value: this.props.value,
+ onChange: this.handleChange,
+ placeholder: "Description",
+ rows: 10,
+ id: "descriptionTextArea"
+ });
+ }
+});
+
+var Checkbox = React.createClass({
+ handleChange: function(event) {
+ this.props.onInput(this.props.keyprop, event.target.checked);
+ },
+ render: function() {
+ return React.DOM.input({
+ type: "checkbox",
+ checked: this.props.checked,
+ onChange: this.handleChange
+ });
+ }
+});
+
+var Dropdown = React.createClass({
+ handleChange: function(event) {
+ this.props.onInput(this.props.keyprop, event.target.value);
+ },
+ render: function() {
+ return React.DOM.select({
+ value: this.props.value,
+ onChange: this.handleChange
+ }, this.props.options.map((option, index) => {
+ return React.DOM.option({
+ // could use option[0] here instead of index...
+ key: this.props.keyprop + "Option" + index,
+ onChange: this.handleChange,
+ value: option[0],
+ disabled: this.props.disabled || false
+ }, option[1]);
+ }));
+ }
+});
+
+var Link = React.createClass({
+ handleClick: function() {
+ this.props.onInput(this.props.value);
+ },
+ render: function() {
+ return React.DOM.a({
+ // href: "",
+ onClick: this.handleClick
+ },
+ this.props.value
+ );
+ }
+});
+
+var DatePicker = React.createClass({
+ render: function() {
+ return React.DOM.input({ type: "date" });
+ }
+});
+
+var Capsule = React.createClass({
+ handleDelete: function() {
+ this.props.onDelete(this.props.keyprop, this.props.value);
+ },
+ render: function() {
+ return React.DOM.span(
+ {
+ className: "capsule",
+ style: { background: "ButtonHighlight" },
+ },
+ this.props.value,
+ React.DOM.span({
+ className: "deleteCapsule",
+ onClick: this.handleDelete
+ },
+ "x"
+ )
+ );
+ }
+});
+
+var TopComponent = React.createClass({
+ getDefaultProps: function() {
+ return {
+ // these "initial" props are passed in as props but
+ // immediately become state (state can change, props do not)
+ initialTitle: "New Event",
+ initialLocation: "",
+ initialAllDay: false,
+ initialRepeat: "none",
+ initialRepeatUntilDate: "forever",
+ initialReminders: 0,
+ initialDescription: "",
+ initialShowTimeAs: "OPAQUE",
+ initialCalendarId: 0,
+ initialPrivacy: 0,
+ initialStatus: 0,
+ initialPriority: 0,
+ initialUrl: "",
+ initialShowUrl: false,
+ initialCategories: [],
+ initialCategoriesList: [],
+ initialAttachments: {},
+
+ tabs: ["Description", "More", "Reminders", "Attachments", "Attendees"],
+ calendarList: [
+ [0, "Home"],
+ [1, "Work"]
+ ],
+ privacyList: [
+ ["NONE", "Not Specified"],
+ ["PUBLIC", "Public Event"],
+ ["CONFIDENTIAL", "Show Time and Date Only"],
+ ["PRIVATE", "Private Event"]
+ ],
+ statusList: [
+ ["NONE", "Not Specified"],
+ ["TENTATIVE", "Tentative"],
+ ["CONFIRMED", "Confirmed"],
+ ["CANCELLED", "Canceled"]
+ ],
+ priorityList: [
+ // XXX what about other numbers?
+ [0, "Not Specified"],
+ [9, "Low"],
+ [5, "Normal"],
+ [1, "High"]
+ ],
+ showTimeAsList: [
+ ["OPAQUE", true],
+ ["TRANSPARENT", false]
+ ],
+ repeatList: [
+ ["none", "Does Not Repeat"],
+ ["daily", "Daily"],
+ ["weekly", "Weekly"],
+ ["every.weekday", "Every Weekday"],
+ ["bi.weekly", "Bi-weekly"],
+ ["monthly", "Monthly"],
+ ["yearly", "Yearly"],
+ ["custom", "Custom..."]
+ ],
+ remindersList: [
+ [0, "No Reminder"],
+ [1, "0 Minutes Before"],
+ [2, "5 Minutes Before"],
+ [3, "15 Minutes Before"],
+ [4, "30 Minutes Before"],
+ [5, "1 Hour Before"],
+ [6, "2 Hours Before"],
+ [7, "12 Hours Before"],
+ [8, "1 Day Before"],
+ [9, "2 Days Before"],
+ [10, "1 Week Before"],
+ [11, "Custom..."]
+ ],
+ supportsPriority: false
+ };
+ },
+ getInitialState: function() {
+ // all the passed-in props that begin with 'initial' become state
+ return {
+ title: this.props.initialTitle,
+ location: this.props.initialLocation,
+ startTimezone: this.props.initialStartTimezone,
+ endTimezone: this.props.initialEndTimezone,
+ startDate: this.props.initialStartDate,
+ startTime: this.props.initialStartTime,
+ endDate: this.props.initialEndDate,
+ endTime: this.props.initialEndTime,
+ allDay: this.props.initialAllDay,
+ repeat: this.props.initialRepeat,
+ repeatUntilDate: this.props.initialRepeatUntilDate,
+ reminders: this.props.initialReminders,
+ description: this.props.initialDescription,
+ showTimeAs: this.props.initialShowTimeAs,
+ calendarId: this.props.initialCalendarId,
+ privacy: this.props.initialPrivacy,
+ status: this.props.initialStatus,
+ priority: this.props.initialPriority,
+ url: this.props.initialUrl,
+ showUrl: this.props.initialShowUrl,
+ categories: this.props.initialCategories,
+ categoriesList: this.props.initialCategoriesList,
+ attachments: this.props.initialAttachments,
+
+ isWideview: (window.innerWidth > 750),
+ activeTab: 0
+ };
+ },
+ updateWideview: function() {
+ let wideview = (window.innerWidth > 750);
+ if (wideview != this.state.isWideview) {
+ this.setState({ isWideview: wideview });
+ }
+ },
+ componentWillMount: function() {
+ this.updateWideview();
+ },
+ componentDidMount: function() {
+ window.addEventListener("resize", this.updateWideview);
+ },
+ componentWillUnmount: function() {
+ window.removeEventListener("resize", this.updateWideview);
+ },
+ exportState: function() {
+ // Use this to access this component's state from above/outside
+ // the react component hierarchy, for example, when saving changes.
+ return this.state;
+ },
+ importState: function(aStateObj) {
+ // Use this to impose state changes from above/outside of the
+ // react component hierarchy.
+ this.setState(aStateObj);
+ },
+ handleSimpleChange: function(aKey, aValue) {
+ let obj = {};
+ obj[aKey] = aValue;
+ this.setState(obj);
+ },
+ handleShowTimeAsChange: function(aKey, aValue) {
+ // convert from true/false to OPAQUE/TRANSPARENT
+ let list = this.props.showTimeAsList;
+ let index = list.findIndex(i => (i[1] == aValue));
+ let newValue = list[index][0];
+ this.handleSimpleChange(aKey, newValue);
+ },
+ linkClicked: function(aValue) {
+
+ },
+ onDeleteCapsule: function(aKey, aValue) {
+ let a = this.state[aKey];
+ let index = a.indexOf(aValue);
+ a.splice(index, 1);
+ this.setState({ aKey: a });
+ },
+ render: function() {
+ // 'key' doesn't seem to work as a prop name (presumably because
+ // already used by react?), so using 'keyprop' instead for now.
+ let titleDiv = React.DOM.div(
+ { id: "titleDiv", className: "box" },
+ "Title ",
+ React.createElement(TextField, {
+ keyprop: "title",
+ value: this.state.title,
+ onInput: this.handleSimpleChange
+ })
+ );
+ let locationDiv = React.DOM.div(
+ { id: "locationDiv", className: "box" },
+ "Location ",
+ React.createElement(TextField, {
+ keyprop: "location",
+ value: this.state.location,
+ onInput: this.handleSimpleChange
+ })
+ );
+ let startDiv = React.DOM.div(
+ { id: "startDiv", className: "box" },
+ "Start ",
+ // React.createElement(DatePicker, { }),
+ React.createElement(TextField, {
+ keyprop: "startDate",
+ value: this.state.startDate,
+ onInput: this.handleSimpleChange
+ }),
+ React.createElement(TextField, {
+ keyprop: "startTime",
+ value: this.state.startTime,
+ onInput: this.handleSimpleChange
+ })
+ );
+ let endDiv = React.DOM.div(
+ { id: "endDiv", className: "box" },
+ "End ",
+ React.createElement(TextField, {
+ keyprop: "endDate",
+ value: this.state.endDate,
+ onInput: this.handleSimpleChange
+ }),
+ React.createElement(TextField, {
+ keyprop: "endTime",
+ value: this.state.endTime,
+ onInput: this.handleSimpleChange
+ })
+ // React.createElement(DatePicker, { }),
+ );
+ let allDayDiv = React.DOM.div(
+ { id: "allDayDiv", className: "box" },
+ React.createElement(Checkbox, {
+ keyprop: "allDay",
+ checked: this.state.allDay,
+ onInput: this.handleSimpleChange
+ }),
+ "All day event"
+ );
+ let repeatDiv = React.DOM.div(
+ { id: "repeatDiv", className: "box" },
+ "Repeat ",
+ React.createElement(Dropdown, {
+ keyprop: "repeat",
+ value: this.state.repeat,
+ options: this.props.repeatList,
+ onInput: this.handleSimpleChange
+ }),
+ (this.state.repeat == "none" ? null : " Until "),
+ (this.state.repeat == "none"
+ ? null
+ : React.createElement(TextField, {
+ keyprop: "repeatUntilDate",
+ value: this.state.repeatUntilDate,
+ onInput: this.handleSimpleChange
+ }))
+ );
+ let calendarDiv = React.DOM.div(
+ { id: "calendarDiv", className: "box" },
+ "Calendar ",
+ React.createElement(Dropdown, {
+ keyprop: "calendarId",
+ value: this.state.calendarId,
+ options: this.props.calendarList,
+ onInput: this.handleSimpleChange
+ })
+ );
+ let categoriesCapsules;
+ if (this.state.categories) {
+ categoriesCapsules =
+ this.state.categories.map((cat, index) => {
+ return React.createElement(Capsule, {
+ // color: this.props.categoryList.color,
+ value: cat,
+ key: cat + "key",
+ keyprop: "categories",
+ onDelete: this.onDeleteCapsule
+ });
+ });
+ } else {
+ categoriesCapsules = null;
+ }
+ let categoriesDiv = React.DOM.div(
+ { id: "categoriesDiv", className: "box" },
+ "Categories ",
+ categoriesCapsules,
+ React.createElement(Link, {
+ value: "Add Categories",
+ onInput: this.linkClicked
+ })
+ );
+ let attendeesDiv = React.DOM.div(
+ { id: "attendeesDiv", className: "box" },
+ "Attendees ",
+ React.createElement(Link, {
+ value: "Add Attendees",
+ onInput: this.linkClicked
+ })
+ );
+ let remindersDiv = React.DOM.div(
+ { id: "remindersDiv", className: "box" },
+ "Reminders ",
+ React.createElement(Dropdown, {
+ keyprop: "reminders",
+ value: this.state.reminders,
+ options: this.props.remindersList,
+ onInput: this.handleSimpleChange
+ })
+ );
+ let attachmentsDiv = React.DOM.div(
+ { id: "attachmentsDiv", className: "box" },
+ "Attachments ",
+ React.createElement(Link, {
+ value: "Add Attachments",
+ onInput: this.linkClicked
+ })
+ );
+ let urlDiv = (this.state.showUrl ?
+ React.DOM.div(
+ { id: "urlDiv", className: "box" },
+ "Event link ",
+ React.createElement(Link, {
+ value: this.state.url,
+ onInput: this.linkClicked
+ }))
+ : null
+ );
+
+ let privacyDiv = React.DOM.div(
+ { id: "privacyDiv", className: "box" },
+ "Privacy ",
+ React.createElement(Dropdown, {
+ keyprop: "privacy",
+ value: this.state.privacy,
+ options: this.props.privacyList,
+ onInput: this.handleSimpleChange
+ })
+ );
+ let statusDiv = React.DOM.div(
+ { id: "statusDiv", className: "box" },
+ "Status ",
+ React.createElement(Dropdown, {
+ keyprop: "status",
+ value: this.state.status,
+ options: this.props.statusList,
+ onInput: this.handleSimpleChange
+ })
+ );
+ let priorityDiv = React.DOM.div(
+ { id: "priorityDiv", className: "box" },
+ "Priority ",
+ React.createElement(Dropdown, {
+ keyprop: "priority",
+ value: this.state.priority,
+ options: this.props.priorityList,
+ onInput: this.handleSimpleChange,
+ disabled: !this.props.supportsPriority
+ })
+ );
+
+ let tIndex = this.props.showTimeAsList.findIndex(i => (i[0] == this.state.showTimeAs));
+ let showTimeAsDiv = React.DOM.div(
+ { id: "showTimeAsDiv", className: "box" },
+ React.createElement(Checkbox, {
+ keyprop: "showTimeAs",
+ checked: (tIndex == -1 ? false : this.props.showTimeAsList[tIndex][1]),
+ onInput: this.handleShowTimeAsChange,
+ options: this.props.showTimeAsList
+ }),
+ "Show Time As Busy"
+ );
+ let descriptionDiv = React.DOM.div(
+ { id: "description", value: "Description" },
+ React.createElement(TextArea, {
+ keyprop: "description",
+ value: this.state.description,
+ onInput: this.handleSimpleChange
+ })
+ );
+
+ if (this.state.isWideview) {
+ // wideview
+ return React.DOM.div(
+ { id: "topwrapper" },
+ React.DOM.div(
+ { className: "wrapper" },
+ titleDiv,
+ startDiv,
+ endDiv,
+ allDayDiv,
+ repeatDiv,
+ attendeesDiv,
+ locationDiv,
+ calendarDiv,
+ categoriesDiv,
+ remindersDiv,
+ attachmentsDiv,
+ urlDiv
+ ),
+ descriptionDiv,
+ React.DOM.div(
+ { className: "wrapper", id: "wrapper2" },
+ privacyDiv,
+ statusDiv,
+ priorityDiv,
+ showTimeAsDiv
+ )
+ );
+ } else {
+ // narrowview
+ let tabpanelChildren = [
+ descriptionDiv,
+ React.DOM.div({
+ className: "wrapper",
+ style: { flexDirection: "column" },
+ }, statusDiv, priorityDiv, showTimeAsDiv),
+ remindersDiv,
+ attachmentsDiv,
+ attendeesDiv
+ ];
+ let tabpanels = this.props.tabs.map((elem, index) => {
+ return React.DOM.div({
+ className: "box tabpanel " + (this.state.activeTab == index ? "" : "hidden"),
+ key: "tabpanelkey " + index
+ }, tabpanelChildren[index]);
+ });
+ return React.DOM.div({ id: "topwrapper" },
+ React.DOM.div({ className: "wrapper" },
+ titleDiv,
+ locationDiv,
+ startDiv,
+ endDiv,
+ allDayDiv,
+ repeatDiv,
+ React.DOM.div(
+ { style: { flexDirection: "row", display: "flex" } },
+ calendarDiv,
+ privacyDiv
+ ),
+ categoriesDiv,
+ React.createElement(
+ Tabstrip, {
+ tabs: this.props.tabs,
+ keyprop: "activeTab",
+ activeTab: this.state.activeTab,
+ onInput: this.handleSimpleChange
+ }),
+ tabpanels,
+ urlDiv
+ )
+ );
+ }
+ }
+});
+
+window.onload = function() {
+ onLoad();
+};
diff --git a/calendar/lightning/content/imip-bar-overlay.xul b/calendar/lightning/content/imip-bar-overlay.xul
new file mode 100644
index 000000000..171bff802
--- /dev/null
+++ b/calendar/lightning/content/imip-bar-overlay.xul
@@ -0,0 +1,266 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd">
+ %lightningDTD;
+]>
+
+<?xml-stylesheet href="chrome://lightning/content/lightning-widgets.css" type="text/css"?>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"
+ src="chrome://lightning/content/lightning-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://lightning/content/imip-bar.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-management.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-ui-utils.js"/>
+
+ <vbox id="messagepanebox">
+ <vbox id="singlemessage" insertbefore="msgHeaderView">
+ <lightning-notification-bar id="imip-bar"
+ collapsed="true"
+ insertbefore="msgHeaderView"
+ label="&lightning.imipbar.description;">
+
+ <!-- Some Toolbox implementation notes:
+ -
+ - css style:
+ - classes within toolbox are making use of existing TB css definitions - as used in
+ - /comm-central/source/mail/base/content/msgHdrViewOverlay.xul, only icon defining
+ - classes like imipAcceptButton are noted separately and OS specific within
+ - skin/lightning.css (resp. the OS-specific theme folders)
+ -
+ - The toolbarbuttons will be adjusted dynamically in imip-bar.js based on their
+ - content of menuitems. To avoid breaking this, the following should be considered
+ - if adding/changing toolbarbutton definitions.
+ - general:
+ - * the toolbarbuttons will appear in order of definition
+ - within the toolbar if visible
+ - * must be hidden by default
+ - * menuitem inside must not be hidden by default
+ - simple button:
+ - * must not have a type attribute
+ - * may have menupopup/menuitem within (not displayed though)
+ - dropdown only:
+ - * must have type=menu
+ - * should have a menupopup with at least one menuitem
+ - smart-dropdown:
+ - * must have type=menu-button
+ - * should have a menupopup with at least one menuitem
+ - * bubbling up of events from menuitems to toolbarbutton must be prevented
+ - by adding a trailing "if (event.target.id == this.id) " to the respective
+ - ltnImipBar.executeAction(...)
+ - * toolbarbutton's oncommand should end with the first menuitem's oncommand
+ - to not break automated conforming
+ - e.g. "if (event.target.id == this.id) ltnImipBar.executeAction('ACCEPTED');"
+ - and "ltnImipBar.executeAction('ACCEPTED');"
+ //-->
+ <toolbox id="imip-view-toolbox"
+ class="inline-toolbox"
+ defaulticonsize="small"
+ minwidth="50px"
+ defaultlabelalign="end"
+ labelalign="end"
+ defaultmode="full"
+ inlinetoolbox="true">
+ <toolbar id="imip-view-toolbar" class="inline-toolbar" align="start"
+ customizable="false" mode="full"
+ defaulticonsize="small" defaultmode="full">
+
+ <!-- show event/invitation details -->
+ <toolbarbutton id="imipDetailsButton"
+ label="&lightning.imipbar.btnDetails.label;"
+ tooltiptext="&lightning.imipbar.btnDetails.tooltiptext;"
+ class="toolbarbutton-1 msgHeaderView-button imipDetailsButton"
+ oncommand="ltnImipBar.executeAction('X-SHOWDETAILS')"
+ hidden="true"/>
+
+ <!-- decline counter -->
+ <toolbarbutton id="imipDeclineCounterButton"
+ label="&lightning.imipbar.btnDeclineCounter.label;"
+ tooltiptext="&lightning.imipbar.btnDeclineCounter.tooltiptext;"
+ class="toolbarbutton-1 msgHeaderView-button imipDeclineCounterButton"
+ oncommand="ltnImipBar.executeAction('X-DECLINECOUNTER')"
+ hidden="true"/>
+
+ <!-- reschedule -->
+ <toolbarbutton id="imipRescheduleButton"
+ label="&lightning.imipbar.btnReschedule.label;"
+ tooltiptext="&lightning.imipbar.btnReschedule.tooltiptext;"
+ class="toolbarbutton-1 msgHeaderView-button imipRescheduleButton"
+ oncommand="ltnImipBar.executeAction('X-RESCHEDULE')"
+ hidden="true"/>
+
+ <!-- add published events -->
+ <toolbarbutton id="imipAddButton"
+ label="&lightning.imipbar.btnAdd.label;"
+ tooltiptext="&lightning.imipbar.btnAdd.tooltiptext;"
+ class="toolbarbutton-1 msgHeaderView-button imipAddButton"
+ oncommand="ltnImipBar.executeAction()"
+ hidden="true"/>
+
+ <!-- update published events and invitations -->
+ <toolbarbutton id="imipUpdateButton"
+ label="&lightning.imipbar.btnUpdate.label;"
+ tooltiptext="&lightning.imipbar.btnUpdate.tooltiptext;"
+ class="toolbarbutton-1 msgHeaderView-button imipUpdateButton"
+ oncommand="ltnImipBar.executeAction()"
+ hidden="true"/>
+
+ <!-- delete cancelled events from calendar -->
+ <toolbarbutton id="imipDeleteButton"
+ label="&lightning.imipbar.btnDelete.label;"
+ tooltiptext="&lightning.imipbar.btnDelete.tooltiptext;"
+ class="toolbarbutton-1 msgHeaderView-button imipDeleteButton"
+ oncommand="ltnImipBar.executeAction()"
+ hidden="true"/>
+
+ <!-- re-confirm partstat -->
+ <toolbarbutton id="imipReconfirmButton"
+ label="&lightning.imipbar.btnReconfirm2.label;"
+ tooltiptext="&lightning.imipbar.btnReconfirm.tooltiptext;"
+ class="toolbarbutton-1 msgHeaderView-button imipReconfirmButton"
+ oncommand="ltnImipBar.executeAction()"
+ hidden="true"/>
+
+ <!-- accept -->
+ <toolbarbutton id="imipAcceptButton"
+ tooltiptext="&lightning.imipbar.btnAccept2.tooltiptext;"
+ label="&lightning.imipbar.btnAccept.label;"
+ oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('ACCEPTED');"
+ type="menu-button"
+ class="imip-button toolbarbutton-1 msgHeaderView-button imipAcceptButton"
+ hidden="true">
+ <menupopup id="imipAcceptDropdown">
+ <menuitem id="imipAcceptButton_Accept"
+ tooltiptext="&lightning.imipbar.btnAccept2.tooltiptext;"
+ label="&lightning.imipbar.btnAccept.label;"
+ oncommand="ltnImipBar.executeAction('ACCEPTED');"/>
+ <menuitem id="imipAcceptButton_Tentative"
+ tooltiptext="&lightning.imipbar.btnTentative2.tooltiptext;"
+ label="&lightning.imipbar.btnTentative.label;"
+ oncommand="ltnImipBar.executeAction('TENTATIVE');"/>
+ <!-- add here more menuitem as needed -->
+ </menupopup>
+ </toolbarbutton>
+
+ <!-- accept recurrences -->
+ <toolbarbutton id="imipAcceptRecurrencesButton"
+ tooltiptext="&lightning.imipbar.btnAcceptRecurrences2.tooltiptext;"
+ label="&lightning.imipbar.btnAcceptRecurrences.label;"
+ oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('ACCEPTED');"
+ type="menu-button"
+ class="imip-button toolbarbutton-1 msgHeaderView-button imipAcceptRecurrencesButton"
+ hidden="true">
+ <menupopup id="imipAcceptRecurrencesDropdown">
+ <menuitem id="imipAcceptRecurrencesButton_Accept"
+ tooltiptext="&lightning.imipbar.btnAcceptRecurrences2.tooltiptext;"
+ label="&lightning.imipbar.btnAcceptRecurrences.label;"
+ oncommand="ltnImipBar.executeAction('ACCEPTED');"/>
+ <menuitem id="imipAcceptRecurrencesButton_Tentative"
+ tooltiptext="&lightning.imipbar.btnTentativeRecurrences2.tooltiptext;"
+ label="&lightning.imipbar.btnTentativeRecurrences.label;"
+ oncommand="ltnImipBar.executeAction('TENTATIVE');"/>
+ <!-- add here more menuitem as needed -->
+ </menupopup>
+ </toolbarbutton>
+
+ <!-- tentative; should only be used, if no imipMoreButton is used and
+ - imipDeclineButton/imipAcceptButton have no visible menuitems //-->
+ <toolbarbutton id="imipTentativeButton"
+ label="&lightning.imipbar.btnTentative.label;"
+ tooltiptext="&lightning.imipbar.btnTentative2.tooltiptext;"
+ class="toolbarbutton-1 msgHeaderView-button imipTentativeButton"
+ oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('TENTATIVE');"
+ type="menu-button"
+ hidden="true">
+ <menupopup id="imipTentativeDropdown">
+ <menuitem id="imipTentativeButton_Tentative"
+ tooltiptext="&lightning.imipbar.btnTentative2.tooltiptext;"
+ label="&lightning.imipbar.btnTentative.label;"
+ oncommand="ltnImipBar.executeAction('TENTATIVE');"/>
+ <!-- add here more menuitem as needed -->
+ </menupopup>
+ </toolbarbutton>
+
+ <!-- tentative recurrences; should only be used, if no imipMoreButton is used and
+ - imipDeclineRecurrencesButton/imipAcceptRecurrencesButton have no visible menuitems //-->
+ <toolbarbutton id="imipTentativeRecurrencesButton"
+ label="&lightning.imipbar.btnTentativeRecurrences.label;"
+ tooltiptext="&lightning.imipbar.btnTentativeRecurrences2.tooltiptext;"
+ class="toolbarbutton-1 msgHeaderView-button imipTentativeRecurrencesButton"
+ oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('TENTATIVE');"
+ type="menu-button"
+ hidden="true">
+ <menupopup id="imipTentativeRecurrencesDropdown">
+ <menuitem id="imipTentativeRecurrencesButton_Tentative"
+ tooltiptext="&lightning.imipbar.btnTentativeRecurrences2.tooltiptext;"
+ label="&lightning.imipbar.btnTentativeRecurrences.label;"
+ oncommand="ltnImipBar.executeAction('TENTATIVE');"/>
+ <!-- add here more menuitem as needed -->
+ </menupopup>
+ </toolbarbutton>
+
+ <!-- decline -->
+ <toolbarbutton id="imipDeclineButton"
+ tooltiptext="&lightning.imipbar.btnDecline2.tooltiptext;"
+ label="&lightning.imipbar.btnDecline.label;"
+ oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('DECLINED');"
+ type="menu-button"
+ class="toolbarbutton-1 msgHeaderView-button imipDeclineButton"
+ hidden="true">
+ <menupopup id="imipDeclineDropdown">
+ <menuitem id="imipDeclineButton_Decline"
+ tooltiptext="&lightning.imipbar.btnDecline2.tooltiptext;"
+ label="&lightning.imipbar.btnDecline.label;"
+ oncommand="ltnImipBar.executeAction('DECLINED');"/>
+ <!-- add here more menuitem as needed -->
+ </menupopup>
+ </toolbarbutton>
+
+ <!-- decline recurrences -->
+ <toolbarbutton id="imipDeclineRecurrencesButton"
+ tooltiptext="&lightning.imipbar.btnDeclineRecurrences2.tooltiptext;"
+ label="&lightning.imipbar.btnDeclineRecurrences.label;"
+ oncommand="if (event.target.id == this.id) ltnImipBar.executeAction('DECLINED');"
+ type="menu-button"
+ class="toolbarbutton-1 msgHeaderView-button imipDeclineRecurrencesButton"
+ hidden="true">
+ <menupopup id="imipDeclineRecurrencesDropdown">
+ <menuitem id="imipDeclineRecurrencesButton_DeclineAll"
+ tooltiptext="&lightning.imipbar.btnDeclineRecurrences2.tooltiptext;"
+ label="&lightning.imipbar.btnDeclineRecurrences.label;"
+ oncommand="ltnImipBar.executeAction('DECLINED');"/>
+ <!-- add here more menuitem as needed -->
+ </menupopup>
+ </toolbarbutton>
+
+ <!-- more options -->
+ <toolbarbutton id="imipMoreButton"
+ type="menu"
+ tooltiptext="&lightning.imipbar.btnMore.tooltiptext;"
+ label="&lightning.imipbar.btnMore.label;"
+ class="toolbarbutton-1 msgHeaderView-button imipMoreButton"
+ hidden="true">
+ <menupopup id="imipMoreDropdown">
+ <menuitem id="imipMoreButton_SaveCopy"
+ tooltiptext="&lightning.imipbar.btnSaveCopy.tooltiptext;"
+ label="&lightning.imipbar.btnSaveCopy.label;"
+ oncommand="ltnImipBar.executeAction('X-SAVECOPY');"/>
+ <!-- add here a menuitem as needed -->
+ </menupopup>
+ </toolbarbutton>
+ </toolbar>
+ </toolbox>
+ </lightning-notification-bar>
+ </vbox>
+ </vbox>
+</overlay>
diff --git a/calendar/lightning/content/imip-bar.js b/calendar/lightning/content/imip-bar.js
new file mode 100644
index 000000000..039e0c36e
--- /dev/null
+++ b/calendar/lightning/content/imip-bar.js
@@ -0,0 +1,470 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calItipUtils.jsm");
+Components.utils.import("resource://calendar/modules/calXMLUtils.jsm");
+Components.utils.import("resource://calendar/modules/ltnInvitationUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * This bar lives inside the message window.
+ * Its lifetime is the lifetime of the main thunderbird message window.
+ */
+var ltnImipBar = {
+
+ actionFunc: null,
+ itipItem: null,
+ foundItems: null,
+ msgOverlay: null,
+
+ /**
+ * Thunderbird Message listener interface, hide the bar before we begin
+ */
+ onStartHeaders: function() {
+ ltnImipBar.resetBar();
+ },
+
+ /**
+ * Thunderbird Message listener interface
+ */
+ onEndHeaders: function() {
+
+ },
+
+ /**
+ * Load Handler called to initialize the imip bar
+ * NOTE: This function is called without a valid this-context!
+ */
+ load: function() {
+ // Add a listener to gMessageListeners defined in msgHdrViewOverlay.js
+ gMessageListeners.push(ltnImipBar);
+
+ // We need to extend the HideMessageHeaderPane function to also hide the
+ // message header pane. Otherwise, the imip bar will still be shown when
+ // changing folders.
+ ltnImipBar.tbHideMessageHeaderPane = HideMessageHeaderPane;
+ HideMessageHeaderPane = function() {
+ ltnImipBar.resetBar();
+ ltnImipBar.tbHideMessageHeaderPane.apply(null, arguments);
+ };
+
+ // Set up our observers
+ Services.obs.addObserver(ltnImipBar, "onItipItemCreation", false);
+ },
+
+ /**
+ * Unload handler to clean up after the imip bar
+ * NOTE: This function is called without a valid this-context!
+ */
+ unload: function() {
+ removeEventListener("messagepane-loaded", ltnImipBar.load, true);
+ removeEventListener("messagepane-unloaded", ltnImipBar.unload, true);
+
+ ltnImipBar.resetBar();
+ Services.obs.removeObserver(ltnImipBar, "onItipItemCreation");
+ },
+
+ observe: function(subject, topic, state) {
+ if (topic == "onItipItemCreation") {
+ let itipItem = null;
+ let msgOverlay = null;
+ try {
+ if (!subject) {
+ let sinkProps = msgWindow.msgHeaderSink.properties;
+ // This property was set by lightningTextCalendarConverter.js
+ itipItem = sinkProps.getPropertyAsInterface("itipItem",
+ Components.interfaces.calIItipItem);
+ msgOverlay = sinkProps.getPropertyAsAUTF8String("msgOverlay");
+ }
+ } catch (e) {
+ // This will throw on every message viewed that doesn't have the
+ // itipItem property set on it. So we eat the errors and move on.
+
+ // XXX TODO: Only swallow the errors we need to. Throw all others.
+ }
+ if (!itipItem || !msgOverlay || !gMessageDisplay.displayedMessage) {
+ return;
+ }
+
+ let imipMethod = gMessageDisplay.displayedMessage.getStringProperty("imip_method");
+ cal.itip.initItemFromMsgData(itipItem, imipMethod, gMessageDisplay.displayedMessage);
+
+ let imipBar = document.getElementById("imip-bar");
+ imipBar.setAttribute("collapsed", "false");
+ imipBar.setAttribute("label", cal.itip.getMethodText(itipItem.receivedMethod));
+
+ ltnImipBar.msgOverlay = msgOverlay;
+
+ cal.itip.processItipItem(itipItem, ltnImipBar.setupOptions);
+ }
+ },
+
+ /**
+ * Hide the imip bar and reset the itip item.
+ */
+ resetBar: function() {
+ document.getElementById("imip-bar").collapsed = true;
+ ltnImipBar.resetButtons();
+
+ // Clear our iMIP/iTIP stuff so it doesn't contain stale information.
+ cal.itip.cleanupItipItem(ltnImipBar.itipItem);
+ ltnImipBar.itipItem = null;
+ },
+
+ /**
+ * Resets all buttons and its menuitems, all buttons are hidden thereafter
+ */
+ resetButtons: function() {
+ let buttons = ltnImipBar.getButtons();
+ buttons.forEach(hideElement);
+ buttons.forEach(aButton => ltnImipBar.getMenuItems(aButton).forEach(showElement));
+ },
+
+ /**
+ * Provides a list of all available buttons
+ */
+ getButtons: function() {
+ let toolbarbuttons = document.getElementById("imip-view-toolbar")
+ .getElementsByTagName("toolbarbutton");
+ return Array.from(toolbarbuttons);
+ },
+
+ /**
+ * Provides a list of available menuitems of a button
+ *
+ * @param aButton button node
+ */
+ getMenuItems: function(aButton) {
+ let items = [];
+ let mitems = aButton.getElementsByTagName("menuitem");
+ if (mitems != null && mitems.length > 0) {
+ for (let mitem of mitems) {
+ items.push(mitem);
+ }
+ }
+ return items;
+ },
+
+ /**
+ * Checks and converts button types based on available menuitems of the buttons
+ * to avoid dropdowns which are empty or only replicating the default button action
+ * Should be called once the buttons are set up
+ */
+ conformButtonType: function() {
+ // check only needed on visible and not simple buttons
+ let buttons = ltnImipBar.getButtons()
+ .filter(aElement => aElement.hasAttribute("type") && !aElement.hidden);
+ // change button if appropriate
+ for (let button of buttons) {
+ let items = ltnImipBar.getMenuItems(button).filter(aItem => !aItem.hidden);
+ if (button.type == "menu" && items.length == 0) {
+ // hide non functional buttons
+ button.hidden = true;
+ } else if (button.type == "menu-button") {
+ if (items.length == 0 ||
+ (items.length == 1 &&
+ button.hasAttribute("oncommand") &&
+ items[0].hasAttribute("oncommand") &&
+ button.getAttribute("oncommand")
+ .endsWith(items[0].getAttribute("oncommand")))) {
+ // convert to simple button
+ button.removeAttribute("type");
+ }
+ }
+ }
+ },
+
+ /**
+ * This is our callback function that is called each time the itip bar UI needs updating.
+ * NOTE: This function is called without a valid this-context!
+ *
+ * @param itipItem The iTIP item to set up for
+ * @param rc The status code from processing
+ * @param actionFunc The action function called for execution
+ * @param foundItems An array of items found while searching for the item
+ * in subscribed calendars
+ */
+ setupOptions: function(itipItem, rc, actionFunc, foundItems) {
+ let imipBar = document.getElementById("imip-bar");
+ let data = cal.itip.getOptionsText(itipItem, rc, actionFunc, foundItems);
+
+ if (Components.isSuccessCode(rc)) {
+ ltnImipBar.itipItem = itipItem;
+ ltnImipBar.actionFunc = actionFunc;
+ ltnImipBar.foundItems = foundItems;
+ }
+
+ // We need this to determine whether this is an outgoing or incoming message because
+ // Thunderbird doesn't provide a distinct flag on message level to do so. Relying on
+ // folder flags only may lead to false positives.
+ let isOutgoing = function(aMsgHdr) {
+ if (!aMsgHdr) {
+ return false;
+ }
+ let author = aMsgHdr.mime2DecodedAuthor;
+ let isSentFolder = aMsgHdr.folder && aMsgHdr.folder.flags &
+ Components.interfaces.nsMsgFolderFlags.SentMail;
+ if (author && isSentFolder) {
+ let accounts = MailServices.accounts;
+ for (let identity in fixIterator(accounts.allIdentities,
+ Components.interfaces.nsIMsgIdentity)) {
+ if (author.includes(identity.email) && !identity.fccReplyFollowsParent) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ // We override the bar label for sent out invitations and in case the event does not exist
+ // anymore, we also clear the buttons if any to avoid e.g. accept/decline buttons
+ if (isOutgoing(gMessageDisplay.displayedMessage)) {
+ if (ltnImipBar.foundItems && ltnImipBar.foundItems[0]) {
+ data.label = ltn.getString("lightning", "imipBarSentText");
+ } else {
+ data = {
+ label: ltn.getString("lightning", "imipBarSentButRemovedText"),
+ buttons: [],
+ hideMenuItems: []
+ };
+ }
+ }
+
+ imipBar.setAttribute("label", data.label);
+ // let's reset all buttons first
+ ltnImipBar.resetButtons();
+ // menu items are visible by default, let's hide what's not available
+ data.hideMenuItems.forEach(aElementId => hideElement(document.getElementById(aElementId)));
+ // buttons are hidden by default, let's make required buttons visible
+ data.buttons.forEach(aElementId => showElement(document.getElementById(aElementId)));
+ // adjust button style if necessary
+ ltnImipBar.conformButtonType();
+ ltnImipBar.displayModifications();
+ },
+
+ /**
+ * Displays changes in case of invitation updates in invitation overlay
+ */
+ displayModifications: function() {
+ if (!ltnImipBar.msgOverlay || !msgWindow || !ltnImipBar.foundItems ||
+ !ltnImipBar.foundItems[0] || !ltnImipBar.itipItem) {
+ return;
+ }
+
+ let msgOverlay = ltnImipBar.msgOverlay;
+ let diff = cal.itip.compare(ltnImipBar.itipItem.getItemList({})[0], ltnImipBar.foundItems[0]);
+ // displaying chnages is only needed if that is enabled, an item already exists and there are
+ // differences
+ if (diff != 0 && Preferences.get("calendar.itip.displayInvitationChanges", false)) {
+ let foundOverlay = ltn.invitation.createInvitationOverlay(ltnImipBar.foundItems[0],
+ ltnImipBar.itipItem);
+ let serializedOverlay = cal.xml.serializeDOM(foundOverlay);
+ let organizerId = ltnImipBar.itipItem.targetCalendar.getProperty("organizerId");
+ if (diff == 1) {
+ // this is an update to previously accepted invitation
+ msgOverlay = ltn.invitation.compareInvitationOverlay(serializedOverlay, msgOverlay,
+ organizerId);
+ } else {
+ // this is a copy of a previously sent out invitation or a previous revision of a
+ // meanwhile accepted invitation, so we flip comparison order
+ msgOverlay = ltn.invitation.compareInvitationOverlay(msgOverlay, serializedOverlay,
+ organizerId);
+ }
+ }
+ msgWindow.displayHTMLInMessagePane("", msgOverlay, false);
+ },
+
+ executeAction: function(partStat, extendResponse) {
+ function _execAction(aActionFunc, aItipItem, aWindow, aPartStat) {
+ if (cal.itip.promptCalendar(aActionFunc.method, aItipItem, aWindow)) {
+ let isDeclineCounter = aPartStat == "X-DECLINECOUNTER";
+ // filter out fake partstats
+ if (aPartStat.startsWith("X-")) {
+ partStat = "";
+ }
+ // hide the buttons now, to disable pressing them twice...
+ if (aPartStat == partStat) {
+ ltnImipBar.resetButtons();
+ }
+
+ let opListener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ if (Components.isSuccessCode(aStatus) && isDeclineCounter) {
+ // TODO: move the DECLINECOUNTER stuff to actionFunc
+ aItipItem.getItemList({}).forEach(aItem => {
+ // we can rely on the received itipItem to reply at this stage
+ // already, the checks have been done in cal.itip.processFoundItems
+ // when setting up the respective aActionFunc
+ let attendees = cal.getAttendeesBySender(
+ aItem.getAttendees({}),
+ aItipItem.sender
+ );
+ let status = true;
+ if (attendees.length == 1 && ltnImipBar.foundItems &&
+ ltnImipBar.foundItems.length) {
+ // we must return a message with the same sequence number as the
+ // counterproposal - to make it easy, we simply use the received
+ // item and just remove a comment, if any
+ try {
+ let item = aItem.clone();
+ item.calendar = ltnImipBar.foundItems[0].calendar;
+ item.deleteProperty("COMMENT");
+ // once we have full support to deal with for multiple items
+ // in a received invitation message, we should send this
+ // from outside outside of the forEach context
+ status = cal.itip.sendDeclineCounterMessage(
+ item,
+ "DECLINECOUNTER",
+ attendees,
+ { value: false }
+ );
+ } catch (e) {
+ cal.ERROR(e);
+ status = false;
+ }
+ } else {
+ status = false;
+ }
+ if (!status) {
+ cal.ERROR("Failed to send DECLINECOUNTER reply!");
+ }
+ });
+ }
+ // For now, we just state the status for the user something very simple
+ let label = cal.itip.getCompleteText(aStatus, aOperationType);
+ imipBar.setAttribute("label", label);
+
+ if (!Components.isSuccessCode(aStatus)) {
+ showError(label);
+ }
+ },
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ }
+ };
+
+ try {
+ aActionFunc(opListener, partStat);
+ } catch (exc) {
+ Components.utils.reportError(exc);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ let imipBar = document.getElementById("imip-bar");
+ if (partStat == null) {
+ partStat = "";
+ }
+ if (partStat == "X-SHOWDETAILS" || partStat == "X-RESCHEDULE") {
+ let counterProposal;
+ let items = ltnImipBar.foundItems;
+ if (items && items.length) {
+ let item = items[0].isMutable ? items[0] : items[0].clone();
+
+ if (partStat == "X-RESCHEDULE") {
+ // TODO most of the following should be moved to the actionFunc defined in
+ // calItipUtils
+ let proposedItem = ltnImipBar.itipItem.getItemList({})[0];
+ let proposedRID = proposedItem.getProperty("RECURRENCE-ID");
+ if (proposedRID) {
+ // if this is a counterproposal for a specific occurrence, we use
+ // that to compare with
+ item = item.recurrenceInfo.getOccurrenceFor(proposedRID).clone();
+ }
+ let parsedProposal = ltn.invitation.parseCounter(proposedItem, item);
+ let potentialProposers = cal.getAttendeesBySender(
+ proposedItem.getAttendees({}),
+ ltnImipBar.itipItem.sender
+ );
+ let proposingAttendee = potentialProposers.length == 1 ?
+ potentialProposers[0] : null;
+ if (proposingAttendee &&
+ ["OK", "OUTDATED", "NOTLATESTUPDATE"].includes(parsedProposal.result.type)) {
+ counterProposal = {
+ attendee: proposingAttendee,
+ proposal: parsedProposal.differences,
+ oldVersion: parsedProposal.result == "OLDVERSION" ||
+ parsedProposal.result == "NOTLATESTUPDATE",
+ onReschedule: () => {
+ imipBar.setAttribute(
+ "label",
+ ltn.getString("lightning", "imipBarCounterPreviousVersionText")
+ );
+ // TODO: should we hide the buttons in this case, too?
+ }
+ };
+ } else {
+ imipBar.setAttribute(
+ "label",
+ ltn.getString("lightning", "imipBarCounterErrorText")
+ );
+ ltnImipBar.resetButtons();
+ if (proposingAttendee) {
+ cal.LOG(parsedProposal.result.descr);
+ } else {
+ cal.LOG("Failed to identify the sending attendee of the counterproposal.");
+ }
+
+ return false;
+ }
+ }
+ // if this a rescheduling operation, we suppress the occurrence prompt here
+ modifyEventWithDialog(item, null, partStat != "X-RESCHEDULE", null, counterProposal);
+ }
+ } else {
+ if (extendResponse) {
+ // Open an extended response dialog to enable the user to add a comment, make a
+ // counterproposal, delegate the event or interact in another way.
+ // Instead of a dialog, this might be implemented as a separate container inside the
+ // imip-overlay as proposed in bug 458578
+ //
+ // If implemented as a dialog, the OL compatibility decision should be incorporated
+ // therein too and the itipItems's autoResponse set to auto subsequently
+ // to prevent a second popup during imip transport processing.
+ }
+ let delmgr = Components.classes["@mozilla.org/calendar/deleted-items-manager;1"]
+ .getService(Components.interfaces.calIDeletedItems);
+ let items = ltnImipBar.itipItem.getItemList({});
+ if (items && items.length) {
+ let delTime = delmgr.getDeletedDate(items[0].id);
+ let dialogText = ltnGetString("lightning", "confirmProcessInvitation");
+ let dialogTitle = ltnGetString("lightning", "confirmProcessInvitationTitle");
+ if (delTime && !Services.prompt.confirm(window, dialogTitle, dialogText)) {
+ return false;
+ }
+ }
+
+ if (partStat == "X-SAVECOPY") {
+ // we create and adopt copies of the respective events
+ let saveitems = ltnImipBar.itipItem.getItemList({}).map(cal.getPublishLikeItemCopy.bind(cal));
+ if (saveitems.length > 0) {
+ let methods = { receivedMethod: "PUBLISH", responseMethod: "PUBLISH" };
+ let newItipItem = cal.itip.getModifiedItipItem(ltnImipBar.itipItem,
+ saveitems, methods);
+ // control to avoid processing _execAction on later user changes on the item
+ let isFirstProcessing = true;
+ // setup callback and trigger re-processing
+ let storeCopy = function(aItipItem, aRc, aActionFunc, aFoundItems) {
+ if (isFirstProcessing && aActionFunc && Components.isSuccessCode(aRc)) {
+ _execAction(aActionFunc, aItipItem, window, partStat);
+ }
+ };
+ cal.itip.processItipItem(newItipItem, storeCopy);
+ isFirstProcessing = false;
+ }
+ // we stop here to not process the original item
+ return false;
+ }
+ return _execAction(ltnImipBar.actionFunc, ltnImipBar.itipItem, window, partStat);
+ }
+ return false;
+ }
+};
+
+addEventListener("messagepane-loaded", ltnImipBar.load, true);
+addEventListener("messagepane-unloaded", ltnImipBar.unload, true);
diff --git a/calendar/lightning/content/lightning-calendar-creation.js b/calendar/lightning/content/lightning-calendar-creation.js
new file mode 100644
index 000000000..e132bf581
--- /dev/null
+++ b/calendar/lightning/content/lightning-calendar-creation.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var common_initCustomizePage = initCustomizePage;
+var common_doCreateCalendar = doCreateCalendar;
+
+initCustomizePage = function() {
+ common_initCustomizePage();
+ ltnInitMailIdentitiesRow();
+};
+
+doCreateCalendar = function() {
+ common_doCreateCalendar();
+ ltnSaveMailIdentitySelection();
+ return true;
+};
diff --git a/calendar/lightning/content/lightning-calendar-creation.xul b/calendar/lightning/content/lightning-calendar-creation.xul
new file mode 100644
index 000000000..e03dd17c7
--- /dev/null
+++ b/calendar/lightning/content/lightning-calendar-creation.xul
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<!DOCTYPE overlay SYSTEM "chrome://lightning/locale/lightning.dtd">
+
+<overlay id="ltnCalendarCreationOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://lightning/content/lightning-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://lightning/content/lightning-calendar-creation.js"/>
+
+ <rows id="customize-rows">
+ <row id="calendar-email-identity-row"
+ align="center"
+ insertafter="customize-suppressAlarms-row">
+ <label value="&lightning.calendarproperties.email.label;"
+ control="email-identity-menulist"/>
+ <menulist id="email-identity-menulist">
+ <menupopup id="email-identity-menupopup"/>
+ </menulist>
+ </row>
+ </rows>
+</overlay>
diff --git a/calendar/lightning/content/lightning-calendar-properties.js b/calendar/lightning/content/lightning-calendar-properties.js
new file mode 100644
index 000000000..99a35367f
--- /dev/null
+++ b/calendar/lightning/content/lightning-calendar-properties.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var common_onLoad = onLoad;
+var common_onAcceptDialog = onAcceptDialog;
+
+onLoad = function() {
+ gCalendar = window.arguments[0].calendar;
+ ltnInitMailIdentitiesRow();
+ common_onLoad();
+};
+
+onAcceptDialog = function() {
+ ltnSaveMailIdentitySelection();
+ return common_onAcceptDialog();
+};
diff --git a/calendar/lightning/content/lightning-calendar-properties.xul b/calendar/lightning/content/lightning-calendar-properties.xul
new file mode 100644
index 000000000..0fdb06b4f
--- /dev/null
+++ b/calendar/lightning/content/lightning-calendar-properties.xul
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<!DOCTYPE overlay SYSTEM "chrome://lightning/locale/lightning.dtd">
+
+<overlay id="ltnCalendarPropertiesOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://lightning/content/lightning-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://lightning/content/lightning-calendar-properties.js"/>
+
+ <rows id="calendar-properties-rows">
+ <row id="calendar-email-identity-row"
+ align="center"
+ insertafter="calendar-uri-row">
+ <label value="&lightning.calendarproperties.email.label;"
+ control="email-identity-menulist"
+ disable-with-calendar="true"/>
+ <menulist id="email-identity-menulist"
+ disable-with-calendar="true">
+ <menupopup id="email-identity-menupopup"/>
+ </menulist>
+ </row>
+ </rows>
+</overlay>
diff --git a/calendar/lightning/content/lightning-invitation.xhtml b/calendar/lightning/content/lightning-invitation.xhtml
new file mode 100644
index 000000000..7d8ccf4f7
--- /dev/null
+++ b/calendar/lightning/content/lightning-invitation.xhtml
@@ -0,0 +1,74 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/>
+ <link rel='stylesheet' type='text/css' href='chrome://messagebody/skin/imip.css'/>
+ </head>
+ <body>
+ <table id="invitation-table">
+ <tr id="imipHtml-header-row">
+ <th colspan="2" class="header">
+ <p id="imipHtml-header-descr" class="header"/>
+ </th>
+ </tr>
+ <tr id="imipHtml-summary-row" hidden="true">
+ <td class="description"><p id="imipHtml-summary-descr"/></td>
+ <td class="content"><p id="imipHtml-summary-content"/></td>
+ </tr>
+ <tr id="imipHtml-location-row" hidden="true">
+ <td class="description"><p id="imipHtml-location-descr"/></td>
+ <td class="content"><p id="imipHtml-location-content"/></td>
+ </tr>
+ <tr id="imipHtml-when-row" hidden="true">
+ <td class="description"><p id="imipHtml-when-descr"/></td>
+ <td class="content"><p id="imipHtml-when-content"/></td>
+ </tr>
+ <tr id="imipHtml-canceledOccurrences-row" hidden="true">
+ <td class="description"><p id="imipHtml-canceledOccurrences-descr"/></td>
+ <td class="content"><p id="imipHtml-canceledOccurrences-content"/></td>
+ </tr>
+ <tr id="imipHtml-modifiedOccurrences-row" hidden="true">
+ <td class="description"><p id="imipHtml-modifiedOccurrences-descr"/></td>
+ <td class="content"><p id="imipHtml-modifiedOccurrences-content"/></td>
+ </tr>
+ <tr id="imipHtml-organizer-row" hidden="true">
+ <td class="description"><p id="imipHtml-organizer-descr"/></td>
+ <td class="content">
+ <table id="organizer-table"/>
+ </td>
+ </tr>
+ <tr id="imipHtml-description-row" hidden="true">
+ <td class="description"><p id="imipHtml-description-descr"/></td>
+ <td class="content"><p id="imipHtml-description-content"/></td>
+ </tr>
+ <tr id="imipHtml-attachments-row" hidden="true">
+ <td class="description"><p id="imipHtml-attachments-descr"/></td>
+ <td class="content"><p id="imipHtml-attachments-content"/></td>
+ </tr>
+ <tr id="imipHtml-comment-row" hidden="true">
+ <td class="description"><p id="imipHtml-comment-descr"/></td>
+ <td class="content"><p id="imipHtml-comment-content"/></td>
+ </tr>
+ <tr id="imipHtml-attendees-row" hidden="true">
+ <td class="description"><p id="imipHtml-attendees-descr"/></td>
+ <td class="content">
+ <table id="attendee-table">
+ <tr id="attendee-template" hidden="true">
+ <td><p class="itip-icon"/></td>
+ <td class="attendee-name"/>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr id="imipHtml-url-row" hidden="true">
+ <td class="description"><p id="imipHtml-url-descr"/></td>
+ <td class="content"><p id="imipHtml-url-content"/></td>
+ </tr>
+ </table>
+ </body>
+</html>
diff --git a/calendar/lightning/content/lightning-item-iframe.js b/calendar/lightning/content/lightning-item-iframe.js
new file mode 100644
index 000000000..800309dd7
--- /dev/null
+++ b/calendar/lightning/content/lightning-item-iframe.js
@@ -0,0 +1,4061 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onEventDialogUnload, changeUndiscloseCheckboxStatus,
+ * toggleKeepDuration, dateTimeControls2State, onUpdateAllDay,
+ * openNewEvent, openNewTask, openNewMessage, openNewCardDialog,
+ * deleteAllAttachments, copyAttachment, attachmentLinkKeyPress,
+ * attachmentDblClick, attachmentClick, notifyUser,
+ * removeNotification, chooseRecentTimezone, showTimezonePopup,
+ * attendeeDblClick, attendeeClick, removeAttendee,
+ * removeAllAttendees, sendMailToUndecidedAttendees, checkUntilDate
+ */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+try {
+ Components.utils.import("resource:///modules/cloudFileAccounts.js");
+} catch (e) {
+ // This will fail on Seamonkey, but thats ok since the pref for cloudfiles
+ // is false, which means the UI will not be shown
+}
+
+// Flag for using new item UI code (HTML/React.js).
+const gNewItemUI = Preferences.get("calendar.item.useNewItemUI", false);
+
+// the following variables are constructed if the jsContext this file
+// belongs to gets constructed. all those variables are meant to be accessed
+// from within this file only.
+var gStartTime = null;
+var gEndTime = null;
+var gItemDuration = null;
+var gStartTimezone = null;
+var gEndTimezone = null;
+var gUntilDate = null;
+var gIsReadOnly = false;
+var gAttachMap = {};
+var gConfirmCancel = true;
+var gLastRepeatSelection = 0;
+var gIgnoreUpdate = false;
+var gWarning = false;
+var gPreviousCalendarId = null;
+var gTabInfoObject;
+var gConfig = {
+ priority: 0,
+ privacy: null,
+ status: "NONE",
+ showTimeAs: null,
+ percentComplete: 0
+};
+// The following variables are set by the load handler function of the
+// parent context, so that they are already set before iframe content load:
+// - gTimezoneEnabled
+// - gShowLink
+
+var eventDialogQuitObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ // Check whether or not we want to veto the quit request (unless another
+ // observer already did.
+ if (aTopic == "quit-application-requested" &&
+ (aSubject instanceof Components.interfaces.nsISupportsPRBool) &&
+ !aSubject.data) {
+ aSubject.data = !onCancel();
+ }
+ }
+};
+
+var eventDialogCalendarObserver = {
+ target: null,
+ isObserving: false,
+
+ onModifyItem: function(aNewItem, aOldItem) {
+ if (this.isObserving && "calendarItem" in window &&
+ window.calendarItem && window.calendarItem.id == aOldItem.id) {
+ let doUpdate = true;
+
+ // The item has been modified outside the dialog. We only need to
+ // prompt if there have been local changes also.
+ if (isItemChanged()) {
+ let promptService = Components.interfaces.nsIPromptService;
+ let promptTitle = calGetString("calendar", "modifyConflictPromptTitle");
+ let promptMessage = calGetString("calendar", "modifyConflictPromptMessage");
+ let promptButton1 = calGetString("calendar", "modifyConflictPromptButton1");
+ let promptButton2 = calGetString("calendar", "modifyConflictPromptButton2");
+ let flags = promptService.BUTTON_TITLE_IS_STRING *
+ promptService.BUTTON_POS_0 +
+ promptService.BUTTON_TITLE_IS_STRING *
+ promptService.BUTTON_POS_1;
+
+ let choice = Services.prompt.confirmEx(window, promptTitle, promptMessage, flags,
+ promptButton1, promptButton2, null, null, {});
+ if (!choice) {
+ doUpdate = false;
+ }
+ }
+
+ let item = aNewItem;
+ if (window.calendarItem.recurrenceId && aNewItem.recurrenceInfo) {
+ item = aNewItem.recurrenceInfo
+ .getOccurrenceFor(window.calendarItem.recurrenceId) || item;
+ }
+ window.calendarItem = item;
+
+ if (doUpdate) {
+ loadDialog(window.calendarItem);
+ }
+ }
+ },
+
+ onDeleteItem: function(aDeletedItem) {
+ if (this.isObserving && "calendarItem" in window &&
+ window.calendarItem && window.calendarItem.id == aDeletedItem.id) {
+ cancelItem();
+ }
+ },
+
+ onStartBatch: function() {},
+ onEndBatch: function() {},
+ onLoad: function() {},
+ onAddItem: function() {},
+ onError: function() {},
+ onPropertyChanged: function() {},
+ onPropertyDeleting: function() {},
+
+ observe: function(aCalendar) {
+ // use the new calendar if one was passed, otherwise use the last one
+ this.target = aCalendar || this.target;
+ if (this.target) {
+ this.cancel();
+ this.target.addObserver(this);
+ this.isObserving = true;
+ }
+ },
+
+ cancel: function() {
+ if (this.isObserving && this.target) {
+ this.target.removeObserver(this);
+ this.isObserving = false;
+ }
+ }
+};
+
+/**
+ * Checks if the given calendar supports notifying attendees. The item is needed
+ * since calendars may support notifications for only some types of items.
+ *
+ * @param {calICalendar} aCalendar The calendar to check
+ * @param {calIItemBase} aItem The item to check support for
+ */
+function canNotifyAttendees(aCalendar, aItem) {
+ try {
+ let calendar = aCalendar.QueryInterface(Components.interfaces.calISchedulingSupport);
+ return (calendar.canNotify("REQUEST", aItem) && calendar.canNotify("CANCEL", aItem));
+ } catch (exc) {
+ return false;
+ }
+}
+
+/**
+ * Sends an asynchronous message to the parent context that contains the
+ * iframe. Additional properties of aMessage are generally arguments
+ * that will be passed to the function named in aMessage.command.
+ *
+ * @param {Object} aMessage The message to pass to the parent context
+ * @param {string} aMessage.command The name of a function to call
+ */
+function sendMessage(aMessage) {
+ parent.postMessage(aMessage, "*");
+}
+
+/**
+ * Receives asynchronous messages from the parent context that contains the iframe.
+ *
+ * @param {MessageEvent} aEvent Contains the message being received
+ */
+function receiveMessage(aEvent) {
+ let validOrigin = gTabmail ? "chrome://messenger" : "chrome://calendar";
+ if (aEvent.origin !== validOrigin) {
+ return;
+ }
+ switch (aEvent.data.command) {
+ case "editAttendees": editAttendees(); break;
+ case "attachURL": attachURL(); break;
+ case "onCommandDeleteItem": onCommandDeleteItem(); break;
+ case "onCommandSave": onCommandSave(aEvent.data.isClosing); break;
+ case "onAccept": onAccept(); break;
+ case "onCancel": onCancel(aEvent.data.iframeId); break;
+ case "openNewEvent": openNewEvent(); break;
+ case "openNewTask": openNewTask(); break;
+ case "editConfigState": {
+ Object.assign(gConfig, aEvent.data.argument);
+ updateConfigState(aEvent.data.argument);
+ if (gNewItemUI) {
+ gTopComponent.importState(aEvent.data.argument);
+ }
+ break;
+ }
+ case "editToDoStatus": {
+ let textbox = document.getElementById("percent-complete-textbox");
+ setElementValue(textbox, aEvent.data.value);
+ updateToDoStatus("percent-changed");
+ break;
+ }
+ case "postponeTask":
+ postponeTask(aEvent.data.value);
+ break;
+ case "toggleTimezoneLinks":
+ gTimezonesEnabled = aEvent.data.checked;
+ updateDateTime();
+ /*
+ // Not implemented in react-code.js yet
+ if (gNewItemUI) {
+ gTopComponent.importState({ timezonesEnabled: aEvent.data.checked });
+ }
+ */
+ break;
+ case "toggleLink": {
+ let newUrl = window.calendarItem.getProperty("URL") || "";
+ let newShow = showOrHideItemURL(aEvent.data.checked, newUrl);
+ // Disable command if there is no url
+ if (!newUrl.length) {
+ sendMessage({ command: "disableLinkCommand" });
+ }
+ if (gNewItemUI) {
+ gTopComponent.importState({
+ url: newUrl,
+ showUrl: newShow
+ });
+ } else {
+ updateItemURL(newShow, newUrl);
+ }
+ break;
+ }
+ case "closingWindowWithTabs": {
+ let response = onCancel(aEvent.data.id, true);
+ sendMessage({
+ command: "replyToClosingWindowWithTabs",
+ response: response
+ });
+ break;
+ }
+ case "attachFileByAccountKey":
+ attachFileByAccountKey(aEvent.data.accountKey);
+ break;
+ }
+}
+
+/**
+ * Sets up the event dialog from the window arguments, also setting up all
+ * dialog controls from the window's item.
+ */
+function onLoad() {
+ window.addEventListener("message", receiveMessage, false);
+
+ // first of all retrieve the array of
+ // arguments this window has been called with.
+ let args = window.arguments[0];
+
+ intializeTabOrWindowVariables();
+
+ // Needed so we can call switchToTab for the prompt about saving
+ // unsaved changes, to show the tab that the prompt is for.
+ if (gInTab) {
+ gTabInfoObject = gTabmail.currentTabInfo;
+ }
+
+ // The calling entity provides us with an object that is responsible
+ // for recording details about the initiated modification. the 'finalize'
+ // property is our hook in order to receive a notification in case the
+ // operation needs to be terminated prematurely. This function will be
+ // called if the calling entity needs to immediately terminate the pending
+ // modification. In this case we serialize the item and close the window.
+ if (args.job) {
+ // keep the iframe id so we can close the right tab...
+ let iframeId = window.frameElement.id;
+
+ // store the 'finalize'-functor in the provided job-object.
+ args.job.finalize = () => {
+ // store any pending modifications...
+ this.onAccept();
+
+ let item = window.calendarItem;
+
+ // ...and close the window.
+ sendMessage({ command: "closeWindowOrTab", iframeId: iframeId });
+
+ return item;
+ };
+ }
+
+ window.fbWrapper = args.fbWrapper;
+
+ // the most important attribute we expect from the
+ // arguments is the item we'll edit in the dialog.
+ let item = args.calendarEvent;
+
+ // set the iframe's top level id for event vs task
+ if (!cal.isEvent(item)) {
+ setDialogId(document.documentElement, "calendar-task-dialog-inner");
+ }
+
+ // new items should have a non-empty title.
+ if (item.isMutable && (!item.title || item.title.length <= 0)) {
+ item.title = cal.calGetString("calendar-event-dialog",
+ isEvent(item) ? "newEvent" : "newTask");
+ }
+
+ window.onAcceptCallback = args.onOk;
+ window.mode = args.mode;
+
+ // we store the item in the window to be able
+ // to access this from any location. please note
+ // that the item is either an occurrence [proxy]
+ // or the stand-alone item [single occurrence item].
+ window.calendarItem = item;
+ // store the initial date value for datepickers in New Task dialog
+ window.initialStartDateValue = args.initialStartDateValue;
+
+ // we store the array of attendees in the window.
+ // clone each existing attendee since we still suffer
+ // from the 'lost x-properties'-bug.
+ window.attendees = [];
+ let attendees = item.getAttendees({});
+ if (attendees && attendees.length) {
+ for (let attendee of attendees) {
+ window.attendees.push(attendee.clone());
+ }
+ }
+
+ window.organizer = null;
+ if (item.organizer) {
+ window.organizer = item.organizer.clone();
+ } else if (item.getAttendees({}).length > 0) {
+ // previous versions of calendar may have filled ORGANIZER correctly on overridden instances:
+ let orgId = item.calendar.getProperty("organizerId");
+ if (orgId) {
+ let organizer = cal.createAttendee();
+ organizer.id = orgId;
+ organizer.commonName = item.calendar.getProperty("organizerCN");
+ organizer.role = "REQ-PARTICIPANT";
+ organizer.participationStatus = "ACCEPTED";
+ organizer.isOrganizer = true;
+ window.organizer = organizer;
+ }
+ }
+
+ // we store the recurrence info in the window so it
+ // can be accessed from any location. since the recurrence
+ // info is a property of the parent item we need to check
+ // whether or not this item is a proxy or a parent.
+ let parentItem = item;
+ if (parentItem.parentItem != parentItem) {
+ parentItem = parentItem.parentItem;
+ }
+
+ window.recurrenceInfo = null;
+ if (parentItem.recurrenceInfo) {
+ window.recurrenceInfo = parentItem.recurrenceInfo.clone();
+ }
+
+ // Set initial values for datepickers in New Tasks dialog
+ if (isToDo(item)) {
+ let initialDatesValue = cal.dateTimeToJsDate(args.initialStartDateValue);
+ if (!gNewItemUI) {
+ setElementValue("completed-date-picker", initialDatesValue);
+ setElementValue("todo-entrydate", initialDatesValue);
+ setElementValue("todo-duedate", initialDatesValue);
+ }
+ }
+ loadDialog(window.calendarItem);
+
+ if (args.counterProposal) {
+ window.counterProposal = args.counterProposal;
+ displayCounterProposal();
+ }
+
+ gMainWindow.setCursor("auto");
+
+ if (!gNewItemUI) {
+ document.getElementById("item-title").focus();
+ document.getElementById("item-title").select();
+ }
+
+ // This causes the app to ask if the window should be closed when the
+ // application is closed.
+ Services.obs.addObserver(eventDialogQuitObserver,
+ "quit-application-requested", false);
+
+ // Normally, Enter closes a <dialog>. We want this to rather on Ctrl+Enter.
+ // Stopping event propagation doesn't seem to work, so just overwrite the
+ // function that does this.
+ if (!gInTab) {
+ document.documentElement._hitEnter = function() {};
+ }
+
+ // set up our calendar event observer
+ eventDialogCalendarObserver.observe(item.calendar);
+
+ onLoad.hasLoaded = true;
+}
+// Set a variable to allow or prevent actions before the dialog is done loading.
+onLoad.hasLoaded = false;
+
+function onEventDialogUnload() {
+ Services.obs.removeObserver(eventDialogQuitObserver,
+ "quit-application-requested");
+ eventDialogCalendarObserver.cancel();
+}
+
+/**
+ * Handler function to be called when the accept button is pressed.
+ *
+ * @return Returns true if the window should be closed
+ */
+function onAccept() {
+ dispose();
+ onCommandSave(true);
+ if (!gWarning) {
+ sendMessage({ command: "closeWindowOrTab" });
+ }
+ return !gWarning;
+}
+
+/**
+ * Asks the user if the item should be saved and does so if requested. If the
+ * user cancels, the window should stay open.
+ *
+ * XXX Could possibly be consolidated into onCancel()
+ *
+ * @return Returns true if the window should be closed.
+ */
+function onCommandCancel() {
+ if (gNewItemUI) {
+ // saving is not supported yet for gNewItemUI, return true to
+ // allow the tab to close
+ console.log("Saving changes is not yet supported with the HTML " +
+ "UI for editing events and tasks.");
+ return true;
+ }
+
+ // Allow closing if the item has not changed and no warning dialog has to be showed.
+ if (!isItemChanged() && !gWarning) {
+ return true;
+ }
+
+ if (gInTab && gTabInfoObject) {
+ // Switch to the tab that the prompt refers to.
+ gTabmail.switchToTab(gTabInfoObject);
+ }
+
+ let promptService = Components.interfaces.nsIPromptService;
+
+ let promptTitle = cal.calGetString("calendar",
+ isEvent(window.calendarItem)
+ ? "askSaveTitleEvent"
+ : "askSaveTitleTask");
+ let promptMessage = cal.calGetString("calendar",
+ isEvent(window.calendarItem)
+ ? "askSaveMessageEvent"
+ : "askSaveMessageTask");
+
+ let flags = promptService.BUTTON_TITLE_SAVE *
+ promptService.BUTTON_POS_0 +
+ promptService.BUTTON_TITLE_CANCEL *
+ promptService.BUTTON_POS_1 +
+ promptService.BUTTON_TITLE_DONT_SAVE *
+ promptService.BUTTON_POS_2;
+
+ let choice = Services.prompt.confirmEx(null,
+ promptTitle,
+ promptMessage,
+ flags,
+ null,
+ null,
+ null,
+ null,
+ {});
+ switch (choice) {
+ case 0: // Save
+ onCommandSave(true);
+ return true;
+ case 2: // Don't save
+ // Don't show any warning dialog when closing without saving.
+ gWarning = false;
+ return true;
+ default: // Cancel
+ return false;
+ }
+}
+
+/**
+ * Handler function to be called when the cancel button is pressed.
+ * aPreventClose is true when closing the main window but leaving the tab open.
+ *
+ * @param {string} aIframeId (optional) iframe id of the tab to be closed
+ * @param {boolean} aPreventClose (optional) True means don't close, just ask about saving
+ * @return {boolean} True if the tab or window should be closed
+ */
+function onCancel(aIframeId, aPreventClose) {
+ // The datepickers need to remove the focus in order to trigger the
+ // validation of the values just edited, with the keyboard, but not yet
+ // confirmed (i.e. not followed by a click, a tab or enter keys pressure).
+ document.documentElement.focus();
+
+ if (!gConfirmCancel || (gConfirmCancel && onCommandCancel())) {
+ dispose();
+ // Don't allow closing the dialog when the user inputs a wrong
+ // date then closes the dialog and answers with "Save" in
+ // the "Save Event" dialog. Don't allow closing the dialog if
+ // the main window is being closed but the tabs in it are not.
+
+ if (!gWarning && aPreventClose != true) {
+ sendMessage({ command: "closeWindowOrTab", iframeId: aIframeId });
+ }
+ return !gWarning;
+ }
+ return false;
+}
+
+/**
+ * Cancels (closes) either the window or the tab, for example when the
+ * item is being deleted.
+ */
+function cancelItem() {
+ gConfirmCancel = false;
+ if (gInTab) {
+ onCancel();
+ } else {
+ sendMessage({ command: "cancelDialog" });
+ }
+}
+
+/**
+ * Sets up all dialog controls from the information of the passed item.
+ *
+ * @param aItem The item to parse information out of.
+ */
+function loadDialog(aItem) {
+ loadDateTime(aItem);
+
+ let itemProps;
+ if (gNewItemUI) {
+ // Properties for initializing the React component/UI.
+ itemProps = {
+ initialTitle: aItem.title,
+ initialLocation: aItem.getProperty("LOCATION"),
+ initialStartTimezone: gStartTimezone,
+ initialEndTimezone: gEndTimezone,
+ initialStartTime: gStartTime,
+ initialEndTime: gEndTime
+ };
+ } else {
+ setElementValue("item-title", aItem.title);
+ setElementValue("item-location", aItem.getProperty("LOCATION"));
+ }
+
+ // add calendars to the calendar menulist
+ if (gNewItemUI) {
+ let calendarToUse = aItem.calendar || window.arguments[0].calendar;
+ let unfilteredList = sortCalendarArray(cal.getCalendarManager().getCalendars({}));
+
+ // filter out calendars that should not be included
+ let calendarList = unfilteredList.filter((calendar) =>
+ (calendar.id == calendarToUse.id ||
+ (calendar &&
+ isCalendarWritable(calendar) &&
+ (userCanAddItemsToCalendar(calendar) ||
+ (calendar == aItem.calendar && userCanModifyItem(aItem))) &&
+ isItemSupported(aItem, calendar))));
+
+ itemProps.calendarList = calendarList.map(calendar => [calendar.id, calendar.name]);
+
+ if (calendarToUse && calendarToUse.id) {
+ let index = itemProps.calendarList.findIndex(
+ calendar => (calendar[0] == calendarToUse.id));
+ if (index != -1) {
+ itemProps.initialCalendarId = calendarToUse.id;
+ }
+ }
+ } else {
+ let calendarList = document.getElementById("item-calendar");
+ removeChildren(calendarList);
+ let indexToSelect = appendCalendarItems(aItem, calendarList, aItem.calendar || window.arguments[0].calendar);
+ if (indexToSelect > -1) {
+ calendarList.selectedIndex = indexToSelect;
+ }
+ }
+
+ // Categories
+ if (gNewItemUI) {
+ // XXX more to do here with localization, see loadCategories.
+ itemProps.initialCategoriesList = cal.sortArrayByLocaleCollator(getPrefCategoriesArray());
+ itemProps.initialCategories = aItem.getCategories({});
+
+ // just to demo capsules component
+ itemProps.initialCategories = ["Some", "Demo", "Categories"];
+ } else {
+ loadCategories(aItem);
+ }
+
+ // Attachment
+ if (!gNewItemUI) {
+ loadCloudProviders();
+ }
+ let hasAttachments = capSupported("attachments");
+ let attachments = aItem.getAttachments({});
+ if (gNewItemUI) {
+ itemProps.initialAttachments = {};
+ }
+ if (hasAttachments && attachments && attachments.length > 0) {
+ for (let attachment of attachments) {
+ if (gNewItemUI) {
+ if (attachment &&
+ attachment.hashId &&
+ !(attachment.hashId in gAttachMap) &&
+ // We currently only support uri attachments.
+ attachment.uri) {
+ itemProps.initialAttachments[attachment.hashId] = attachment;
+
+ // XXX eventually we probably need to call addAttachment(attachment)
+ // here, until this works we just call updateAttachment()
+ updateAttachment();
+ }
+ } else {
+ addAttachment(attachment);
+ }
+ }
+ } else {
+ updateAttachment();
+ }
+
+ // URL link
+ // Currently we always show the link for the tab case (if the link
+ // exists), since there is no menu item or toolbar item to show/hide it.
+ let showLink = gInTab ? true : gShowLink;
+ let itemUrl = window.calendarItem.getProperty("URL") || "";
+ showLink = showOrHideItemURL(showLink, itemUrl);
+
+ // Disable link command if there is no url
+ if (!itemUrl.length) {
+ sendMessage({ command: "disableLinkCommand" });
+ }
+ if (gNewItemUI) {
+ itemProps.initialUrl = itemUrl;
+ itemProps.initialShowUrl = showLink;
+ } else {
+ updateItemURL(showLink, itemUrl);
+ }
+
+ // Description
+ if (gNewItemUI) {
+ itemProps.initialDescription = aItem.getProperty("DESCRIPTION");
+ } else {
+ setElementValue("item-description", aItem.getProperty("DESCRIPTION"));
+ }
+
+ // Task completed date
+ if (!gNewItemUI) {
+ if (aItem.completedDate) {
+ updateToDoStatus(aItem.status, cal.dateTimeToJsDate(aItem.completedDate));
+ } else {
+ updateToDoStatus(aItem.status);
+ }
+ }
+
+ // Task percent complete
+ if (isToDo(aItem)) {
+ let percentCompleteInteger = 0;
+ let percentCompleteProperty = aItem.getProperty("PERCENT-COMPLETE");
+ if (percentCompleteProperty != null) {
+ percentCompleteInteger = parseInt(percentCompleteProperty, 10);
+ }
+ if (percentCompleteInteger < 0) {
+ percentCompleteInteger = 0;
+ } else if (percentCompleteInteger > 100) {
+ percentCompleteInteger = 100;
+ }
+ gConfig.percentComplete = percentCompleteInteger;
+ if (gNewItemUI) {
+ itemProps.initialPercentComplete = percentCompleteInteger;
+ } else {
+ setElementValue("percent-complete-textbox", percentCompleteInteger);
+ }
+ }
+
+ // When in a window, set Item-Menu label to Event or Task
+ if (!gInTab) {
+ let isEvent = cal.isEvent(aItem);
+
+ let labelString = isEvent ? "itemMenuLabelEvent" : "itemMenuLabelTask";
+ let label = cal.calGetString("calendar-event-dialog", labelString);
+
+ let accessKeyString = isEvent ? "itemMenuAccesskeyEvent2" : "itemMenuAccesskeyTask2";
+ let accessKey = cal.calGetString("calendar-event-dialog", accessKeyString);
+ sendMessage({
+ command: "initializeItemMenu",
+ label: label,
+ accessKey: accessKey
+ });
+ }
+
+ // Repeat details
+ let [repeatType, untilDate] = getRepeatTypeAndUntilDate(aItem);
+ if (gNewItemUI) {
+ itemProps.initialRepeat = repeatType;
+ itemProps.initialRepeatUntilDate = untilDate;
+ // XXX more to do, see loadRepeat
+ } else {
+ loadRepeat(repeatType, untilDate, aItem);
+ }
+
+ if (!gNewItemUI) {
+ // load reminders details
+ loadReminders(aItem.getAlarms({}));
+
+ // Synchronize link-top-image with keep-duration-button status
+ let keepAttribute = document.getElementById("keepduration-button").getAttribute("keep") == "true";
+ setBooleanAttribute("link-image-top", "keep", keepAttribute);
+
+ updateDateTime();
+
+ updateCalendar();
+
+ // figure out what the title of the dialog should be and set it
+ // tabs already have their title set
+ if (!gInTab) {
+ updateTitle();
+ }
+
+ let notifyCheckbox = document.getElementById("notify-attendees-checkbox");
+ let undiscloseCheckbox = document.getElementById("undisclose-attendees-checkbox");
+ let disallowcounterCheckbox = document.getElementById("disallow-counter-checkbox");
+ if (canNotifyAttendees(aItem.calendar, aItem)) {
+ // visualize that the server will send out mail:
+ notifyCheckbox.checked = true;
+ // hide these controls as this a client only feature
+ undiscloseCheckbox.disabled = true;
+ } else {
+ let itemProp = aItem.getProperty("X-MOZ-SEND-INVITATIONS");
+ notifyCheckbox.checked = (aItem.calendar.getProperty("imip.identity") &&
+ ((itemProp === null)
+ ? Preferences.get("calendar.itip.notify", true)
+ : (itemProp == "TRUE")));
+ let undiscloseProp = aItem.getProperty("X-MOZ-SEND-INVITATIONS-UNDISCLOSED");
+ undiscloseCheckbox.checked = (undiscloseProp === null)
+ ? false // default value as most common within organizations
+ : (undiscloseProp == "TRUE");
+ // disable checkbox, if notifyCheckbox is not checked
+ undiscloseCheckbox.disabled = (notifyCheckbox.checked == false);
+ }
+ // this may also be a server exposed calendar property from exchange servers - if so, this
+ // probably should overrule the client-side config option
+ let disallowCounterProp = aItem.getProperty("X-MICROSOFT-DISALLOW-COUNTER");
+ disallowcounterCheckbox.checked = disallowCounterProp == "TRUE";
+ // if we're in reschedule mode, it's pointless to enable the control
+ disallowcounterCheckbox.disabled = !!window.counterProposal;
+
+ updateAttendees();
+ updateRepeat(true);
+ updateReminder(true);
+ }
+
+ // Status
+ if (cal.isEvent(aItem)) {
+ gConfig.status = aItem.hasProperty("STATUS") ?
+ aItem.getProperty("STATUS") : "NONE";
+ if (gConfig.status == "NONE") {
+ sendMessage({ command: "showCmdStatusNone" });
+ }
+ updateConfigState({ status: gConfig.status });
+ if (gNewItemUI) {
+ itemProps.initialStatus = gConfig.status;
+ }
+ } else {
+ let itemStatus = aItem.getProperty("STATUS");
+ if (gNewItemUI) {
+ // Not implemented yet in react-code.js
+ // itemProps.initialTodoStatus = itemStatus;
+ } else {
+ let todoStatus = document.getElementById("todo-status");
+ setElementValue(todoStatus, itemStatus);
+ if (!todoStatus.selectedItem) {
+ // No selected item means there was no <menuitem> that matches the
+ // value given. Select the "NONE" item by default.
+ setElementValue(todoStatus, "NONE");
+ }
+ }
+ }
+
+ // Priority, Privacy, Transparency
+ gConfig.priority = parseInt(aItem.priority, 10);
+ gConfig.privacy = aItem.privacy;
+ gConfig.showTimeAs = aItem.getProperty("TRANSP");
+
+ // update in outer parent context
+ updateConfigState(gConfig);
+
+ // update in iframe (gNewItemUI only)
+ if (gNewItemUI) {
+ itemProps.initialPriority = gConfig.priority;
+ itemProps.supportsPriority = capSupported("priority");
+
+ itemProps.initialPrivacy = gConfig.privacy || "NONE";
+ // XXX need to update the privacy options depending on calendar support for them
+ itemProps.supportsPrivacy = capSupported("privacy");
+
+ itemProps.initialShowTimeAs = gConfig.showTimeAs;
+ }
+
+ // render the UI for gNewItemUI
+ if (gNewItemUI) {
+ gTopComponent = ReactDOM.render(
+ React.createElement(TopComponent, itemProps),
+ document.getElementById("container")
+ );
+ }
+}
+
+/**
+ * Enables/disables undiscloseCheckbox on (un)checking notifyCheckbox
+ */
+function changeUndiscloseCheckboxStatus() {
+ let notifyCheckbox = document.getElementById("notify-attendees-checkbox");
+ let undiscloseCheckbox = document.getElementById("undisclose-attendees-checkbox");
+ undiscloseCheckbox.disabled = (!notifyCheckbox.checked);
+}
+
+/**
+ * Loads the item's categories into the category panel
+ *
+ * @param aItem The item to load into the category panel
+ */
+function loadCategories(aItem) {
+ let categoryPanel = document.getElementById("item-categories-panel");
+ categoryPanel.loadItem(aItem);
+ updateCategoryMenulist();
+}
+
+/**
+ * Updates the category menulist to show the correct label, depending on the
+ * selected categories in the category panel
+ */
+function updateCategoryMenulist() {
+ let categoryMenulist = document.getElementById("item-categories");
+ let categoryPanel = document.getElementById("item-categories-panel");
+
+ // Make sure the maximum number of categories is applied to the listbox
+ let calendar = getCurrentCalendar();
+ let maxCount = calendar.getProperty("capabilities.categories.maxCount");
+ categoryPanel.maxCount = (maxCount === null ? -1 : maxCount);
+
+ // Hide the categories listbox and label in case categories are not
+ // supported
+ setBooleanAttribute("item-categories", "hidden", (maxCount === 0));
+ setBooleanAttribute("item-categories-label", "hidden", (maxCount === 0));
+ setBooleanAttribute("item-calendar-label", "hidden", (maxCount === 0));
+ setBooleanAttribute("item-calendar-aux-label", "hidden", (maxCount !== 0));
+
+ let label;
+ let categoryList = categoryPanel.categories;
+ if (categoryList.length > 1) {
+ label = cal.calGetString("calendar", "multipleCategories");
+ } else if (categoryList.length == 1) {
+ label = categoryList[0];
+ } else {
+ label = cal.calGetString("calendar", "None");
+ }
+ categoryMenulist.setAttribute("label", label);
+}
+
+/**
+ * Saves the selected categories into the passed item
+ *
+ * @param aItem The item to set the categories on
+ */
+function saveCategories(aItem) {
+ let categoryPanel = document.getElementById("item-categories-panel");
+ let categoryList = categoryPanel.categories;
+ aItem.setCategories(categoryList.length, categoryList);
+}
+
+/**
+ * Sets up all date related controls from the passed item
+ *
+ * @param item The item to parse information out of.
+ */
+function loadDateTime(item) {
+ let kDefaultTimezone = calendarDefaultTimezone();
+ if (isEvent(item)) {
+ let startTime = item.startDate;
+ let endTime = item.endDate;
+ let duration = endTime.subtractDate(startTime);
+
+ // Check if an all-day event has been passed in (to adapt endDate).
+ if (startTime.isDate) {
+ startTime = startTime.clone();
+ endTime = endTime.clone();
+
+ endTime.day--;
+ duration.days--;
+ }
+
+ // store the start/end-times as calIDateTime-objects
+ // converted to the default timezone. store the timezones
+ // separately.
+ gStartTimezone = startTime.timezone;
+ gEndTimezone = endTime.timezone;
+ gStartTime = startTime.getInTimezone(kDefaultTimezone);
+ gEndTime = endTime.getInTimezone(kDefaultTimezone);
+ gItemDuration = duration;
+ }
+
+ if (isToDo(item)) {
+ let startTime = null;
+ let endTime = null;
+ let duration = null;
+
+ let hasEntryDate = (item.entryDate != null);
+ if (hasEntryDate) {
+ startTime = item.entryDate;
+ gStartTimezone = startTime.timezone;
+ startTime = startTime.getInTimezone(kDefaultTimezone);
+ } else {
+ gStartTimezone = kDefaultTimezone;
+ }
+ let hasDueDate = (item.dueDate != null);
+ if (hasDueDate) {
+ endTime = item.dueDate;
+ gEndTimezone = endTime.timezone;
+ endTime = endTime.getInTimezone(kDefaultTimezone);
+ } else {
+ gEndTimezone = kDefaultTimezone;
+ }
+ if (hasEntryDate && hasDueDate) {
+ duration = endTime.subtractDate(startTime);
+ }
+ if (!gNewItemUI) {
+ setElementValue("cmd_attendees", true, "disabled");
+ setBooleanAttribute("keepduration-button", "disabled", !(hasEntryDate && hasDueDate));
+ }
+ sendMessage({
+ command: "updateConfigState",
+ argument: { attendeesCommand: false }
+ });
+ gStartTime = startTime;
+ gEndTime = endTime;
+ gItemDuration = duration;
+ } else {
+ sendMessage({
+ command: "updateConfigState",
+ argument: { attendeesCommand: true }
+ });
+ }
+}
+
+/**
+ * Toggles the "keep" attribute every time the keepduration-button is pressed.
+ */
+function toggleKeepDuration() {
+ let kdb = document.getElementById("keepduration-button");
+ let keepAttribute = kdb.getAttribute("keep") == "true";
+ // To make the "keep" attribute persistent, it mustn't be removed when in
+ // false state (bug 15232).
+ kdb.setAttribute("keep", keepAttribute ? "false" : "true");
+ setBooleanAttribute("link-image-top", "keep", !keepAttribute);
+}
+
+/**
+ * Handler function to be used when the Start time or End time of the event have
+ * changed.
+ * When changing the Start date, the End date changes automatically so the
+ * event/task's duration stays the same. Instead the End date is not linked
+ * to the Start date unless the the keepDurationButton has the "keep" attribute
+ * set to true. In this case modifying the End date changes the Start date in
+ * order to keep the same duration.
+ *
+ * @param aStartDatepicker If true the Start or Entry datepicker has changed,
+ * otherwise the End or Due datepicker has changed.
+ */
+function dateTimeControls2State(aStartDatepicker) {
+ if (gIgnoreUpdate) {
+ return;
+ }
+ let keepAttribute = document.getElementById("keepduration-button")
+ .getAttribute("keep") == "true";
+ let allDay = getElementValue("event-all-day", "checked");
+ let startWidgetId;
+ let endWidgetId;
+ if (isEvent(window.calendarItem)) {
+ startWidgetId = "event-starttime";
+ endWidgetId = "event-endtime";
+ } else {
+ if (!getElementValue("todo-has-entrydate", "checked")) {
+ gItemDuration = null;
+ }
+ if (!getElementValue("todo-has-duedate", "checked")) {
+ gItemDuration = null;
+ }
+ startWidgetId = "todo-entrydate";
+ endWidgetId = "todo-duedate";
+ }
+
+ let saveStartTime = gStartTime;
+ let saveEndTime = gEndTime;
+ let kDefaultTimezone = calendarDefaultTimezone();
+
+ if (gStartTime) {
+ // jsDate is always in OS timezone, thus we create a calIDateTime
+ // object from the jsDate representation then we convert the timezone
+ // in order to keep gStartTime in default timezone.
+ if (gTimezonesEnabled || allDay) {
+ gStartTime = cal.jsDateToDateTime(getElementValue(startWidgetId), gStartTimezone);
+ gStartTime = gStartTime.getInTimezone(kDefaultTimezone);
+ } else {
+ gStartTime = cal.jsDateToDateTime(getElementValue(startWidgetId), kDefaultTimezone);
+ }
+ gStartTime.isDate = allDay;
+ }
+ if (gEndTime) {
+ if (aStartDatepicker) {
+ // Change the End date in order to keep the duration.
+ gEndTime = gStartTime.clone();
+ if (gItemDuration) {
+ gEndTime.addDuration(gItemDuration);
+ }
+ } else {
+ let timezone = gEndTimezone;
+ if (timezone.isUTC) {
+ if (gStartTime && !compareObjects(gStartTimezone, gEndTimezone)) {
+ timezone = gStartTimezone;
+ }
+ }
+ if (gTimezonesEnabled || allDay) {
+ gEndTime = cal.jsDateToDateTime(getElementValue(endWidgetId), timezone);
+ gEndTime = gEndTime.getInTimezone(kDefaultTimezone);
+ } else {
+ gEndTime = cal.jsDateToDateTime(getElementValue(endWidgetId), kDefaultTimezone);
+ }
+ gEndTime.isDate = allDay;
+ if (keepAttribute && gItemDuration) {
+ // Keepduration-button links the the Start to the End date. We
+ // have to change the Start date in order to keep the duration.
+ let fduration = gItemDuration.clone();
+ fduration.isNegative = true;
+ gStartTime = gEndTime.clone();
+ gStartTime.addDuration(fduration);
+ }
+ }
+ }
+
+ if (allDay) {
+ gStartTime.isDate = true;
+ gEndTime.isDate = true;
+ gItemDuration = gEndTime.subtractDate(gStartTime);
+ }
+
+ // calculate the new duration of start/end-time.
+ // don't allow for negative durations.
+ let warning = false;
+ let stringWarning = "";
+ if (!aStartDatepicker && gStartTime && gEndTime) {
+ if (gEndTime.compare(gStartTime) >= 0) {
+ gItemDuration = gEndTime.subtractDate(gStartTime);
+ } else {
+ gStartTime = saveStartTime;
+ gEndTime = saveEndTime;
+ warning = true;
+ stringWarning = cal.calGetString("calendar", "warningEndBeforeStart");
+ }
+ }
+
+ let startChanged = false;
+ if (gStartTime && saveStartTime) {
+ startChanged = gStartTime.compare(saveStartTime) != 0;
+ }
+ // Preset the date in the until-datepicker's minimonth to the new start
+ // date if it has changed.
+ if (startChanged) {
+ let startDate = cal.dateTimeToJsDate(gStartTime.getInTimezone(cal.floating()));
+ document.getElementById("repeat-until-datepicker").extraDate = startDate;
+ }
+
+ // Sort out and verify the until date if the start date has changed.
+ if (gUntilDate && startChanged) {
+ // Make the time part of the until date equal to the time of start date.
+ updateUntildateRecRule();
+
+ // Don't allow for until date earlier than the start date.
+ if (gUntilDate.compare(gStartTime) < 0) {
+ // We have to restore valid dates. Since the user has intentionally
+ // changed the start date, it looks reasonable to restore a valid
+ // until date equal to the start date.
+ gUntilDate = gStartTime.clone();
+ // Update the rule.
+ let rrules = splitRecurrenceRules(window.recurrenceInfo);
+ recRule = rrules[0][0];
+ recRule.untilDate = gUntilDate.clone();
+ // Update the until-date-picker. In case of "custom" rule, the
+ // recurrence string is going to be changed by updateDateTime() below.
+ let notCustomRule = document.getElementById("repeat-deck").selectedIndex == 0;
+ if (notCustomRule) {
+ setElementValue("repeat-until-datepicker",
+ cal.dateTimeToJsDate(gUntilDate.getInTimezone(cal.floating())));
+ }
+
+ warning = true;
+ stringWarning = cal.calGetString("calendar", "warningUntilDateBeforeStart");
+ }
+ }
+
+ updateDateTime();
+ updateTimezone();
+ updateAccept();
+
+ if (warning) {
+ // Disable the "Save" and "Save and Close" commands as long as the
+ // warning dialog is showed.
+ enableAcceptCommand(false);
+ gWarning = true;
+ let callback = function() {
+ Services.prompt.alert(null, document.title, stringWarning);
+ gWarning = false;
+ updateAccept();
+ };
+ setTimeout(callback, 1);
+ }
+}
+
+/**
+ * Updates the entry date checkboxes, used for example when choosing an alarm:
+ * the entry date needs to be checked in that case.
+ */
+function updateEntryDate() {
+ updateDateCheckboxes(
+ "todo-entrydate",
+ "todo-has-entrydate",
+ {
+ isValid: function() {
+ return gStartTime != null;
+ },
+ setDateTime: function(date) {
+ gStartTime = date;
+ }
+ });
+}
+
+/**
+ * Updates the due date checkboxes.
+ */
+function updateDueDate() {
+ updateDateCheckboxes(
+ "todo-duedate",
+ "todo-has-duedate",
+ {
+ isValid: function() {
+ return gEndTime != null;
+ },
+ setDateTime: function(date) {
+ gEndTime = date;
+ }
+ });
+}
+
+/**
+ * Common function used by updateEntryDate and updateDueDate to set up the
+ * checkboxes correctly.
+ *
+ * @param aDatePickerId The XUL id of the datepicker to update.
+ * @param aCheckboxId The XUL id of the corresponding checkbox.
+ * @param aDateTime An object implementing the isValid and setDateTime
+ * methods. XXX explain.
+ */
+function updateDateCheckboxes(aDatePickerId, aCheckboxId, aDateTime) {
+ if (gIgnoreUpdate) {
+ return;
+ }
+
+ if (!isToDo(window.calendarItem)) {
+ return;
+ }
+
+ // force something to get set if there was nothing there before
+ setElementValue(aDatePickerId, getElementValue(aDatePickerId));
+
+ // first of all disable the datetime picker if we don't have a date
+ let hasDate = getElementValue(aCheckboxId, "checked");
+ setElementValue(aDatePickerId, !hasDate, "disabled");
+
+ // create a new datetime object if date is now checked for the first time
+ if (hasDate && !aDateTime.isValid()) {
+ let date = cal.jsDateToDateTime(getElementValue(aDatePickerId), cal.calendarDefaultTimezone());
+ aDateTime.setDateTime(date);
+ } else if (!hasDate && aDateTime.isValid()) {
+ aDateTime.setDateTime(null);
+ }
+
+ // calculate the duration if possible
+ let hasEntryDate = getElementValue("todo-has-entrydate", "checked");
+ let hasDueDate = getElementValue("todo-has-duedate", "checked");
+ if (hasEntryDate && hasDueDate) {
+ let start = cal.jsDateToDateTime(getElementValue("todo-entrydate"));
+ let end = cal.jsDateToDateTime(getElementValue("todo-duedate"));
+ gItemDuration = end.subtractDate(start);
+ } else {
+ gItemDuration = null;
+ }
+ setBooleanAttribute("keepduration-button", "disabled", !(hasEntryDate && hasDueDate));
+ updateDateTime();
+ updateTimezone();
+}
+
+/**
+ * Get the item's recurrence information for displaying in dialog controls.
+ *
+ * @param {Object} aItem The calendar item
+ * @return {string[]} An array of two strings: [repeatType, untilDate]
+ */
+function getRepeatTypeAndUntilDate(aItem) {
+ let recurrenceInfo = window.recurrenceInfo;
+ let repeatType = "none";
+ let untilDate = "forever";
+
+ /**
+ * Updates the until date (locally and globally).
+ *
+ * @param aRule The recurrence rule
+ */
+ let updateUntilDate = (aRule) => {
+ if (!aRule.isByCount) {
+ if (aRule.isFinite) {
+ gUntilDate = aRule.untilDate.clone().getInTimezone(cal.calendarDefaultTimezone());
+ untilDate = cal.dateTimeToJsDate(gUntilDate.getInTimezone(cal.floating()));
+ } else {
+ gUntilDate = null;
+ }
+ }
+ };
+
+ if (recurrenceInfo) {
+ repeatType = "custom";
+ let ritems = recurrenceInfo.getRecurrenceItems({});
+ let rules = [];
+ let exceptions = [];
+ for (let ritem of ritems) {
+ if (ritem.isNegative) {
+ exceptions.push(ritem);
+ } else {
+ rules.push(ritem);
+ }
+ }
+ if (rules.length == 1) {
+ let rule = cal.wrapInstance(rules[0], Components.interfaces.calIRecurrenceRule);
+ if (rule) {
+ switch (rule.type) {
+ case "DAILY":
+ if (!checkRecurrenceRule(rule, ["BYSECOND",
+ "BYMINUTE",
+ "BYHOUR",
+ "BYMONTHDAY",
+ "BYYEARDAY",
+ "BYWEEKNO",
+ "BYMONTH",
+ "BYSETPOS"])) {
+ let ruleComp = rule.getComponent("BYDAY", {});
+ if (rule.interval == 1) {
+ if (ruleComp.length > 0) {
+ if (ruleComp.length == 5) {
+ let found = false;
+ for (let i = 0; i < 5; i++) {
+ if (ruleComp[i] != i + 2) {
+ found = true;
+ break;
+ }
+ }
+ if (!found && (!rule.isFinite || !rule.isByCount)) {
+ repeatType = "every.weekday";
+ updateUntilDate(rule);
+ }
+ }
+ } else if (!rule.isFinite || !rule.isByCount) {
+ repeatType = "daily";
+ updateUntilDate(rule);
+ }
+ }
+ }
+ break;
+ case "WEEKLY":
+ if (!checkRecurrenceRule(rule, ["BYSECOND",
+ "BYMINUTE",
+ "BYDAY",
+ "BYHOUR",
+ "BYMONTHDAY",
+ "BYYEARDAY",
+ "BYWEEKNO",
+ "BYMONTH",
+ "BYSETPOS"])) {
+ let weekType=["weekly", "bi.weekly"];
+ if ((rule.interval == 1 || rule.interval == 2) &&
+ (!rule.isFinite || !rule.isByCount)) {
+ repeatType = weekType[rule.interval - 1];
+ updateUntilDate(rule);
+ }
+ }
+ break;
+ case "MONTHLY":
+ if (!checkRecurrenceRule(rule, ["BYSECOND",
+ "BYMINUTE",
+ "BYDAY",
+ "BYHOUR",
+ "BYMONTHDAY",
+ "BYYEARDAY",
+ "BYWEEKNO",
+ "BYMONTH",
+ "BYSETPOS"])) {
+ if (rule.interval == 1 && (!rule.isFinite || !rule.isByCount)) {
+ repeatType = "monthly";
+ updateUntilDate(rule);
+ }
+ }
+ break;
+ case "YEARLY":
+ if (!checkRecurrenceRule(rule, ["BYSECOND",
+ "BYMINUTE",
+ "BYDAY",
+ "BYHOUR",
+ "BYMONTHDAY",
+ "BYYEARDAY",
+ "BYWEEKNO",
+ "BYMONTH",
+ "BYSETPOS"])) {
+ if (rule.interval == 1 && (!rule.isFinite || !rule.isByCount)) {
+ repeatType = "yearly";
+ updateUntilDate(rule);
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ return [repeatType, untilDate];
+}
+
+/**
+ * Updates the XUL UI with the repeat type and the until date.
+ *
+ * XXX For gNewItemUI we need to handle gLastRepeatSelection and
+ * disabling the element as we do in this function.
+ *
+ * @param {string} aRepeatType The type of repeat
+ * @param {string} aUntilDate The until date
+ * @param {Object} aItem The calendar item
+ */
+function loadRepeat(aRepeatType, aUntilDate, aItem) {
+ setElementValue("item-repeat", aRepeatType);
+ let repeatMenu = document.getElementById("item-repeat");
+ gLastRepeatSelection = repeatMenu.selectedIndex;
+
+ if (aItem.parentItem != aItem) {
+ disableElement("item-repeat");
+ disableElement("repeat-until-datepicker");
+ }
+ // Show the repeat-until-datepicker and set its date
+ document.getElementById("repeat-deck").selectedIndex = 0;
+ setElementValue("repeat-until-datepicker", aUntilDate);
+}
+
+/**
+ * Update reminder related elements on the dialog.
+ *
+ * @param aSuppressDialogs If true, controls are updated without prompting
+ * for changes with the custom dialog
+ */
+function updateReminder(aSuppressDialogs) {
+ commonUpdateReminder(aSuppressDialogs);
+ updateAccept();
+}
+
+/**
+ * Saves all values the user chose on the dialog to the passed item
+ *
+ * @param item The item to save to.
+ */
+function saveDialog(item) {
+ // Calendar
+ item.calendar = getCurrentCalendar();
+
+ setItemProperty(item, "title", getElementValue("item-title"));
+ setItemProperty(item, "LOCATION", getElementValue("item-location"));
+
+ saveDateTime(item);
+
+ if (isToDo(item)) {
+ let percentCompleteInteger = 0;
+ if (getElementValue("percent-complete-textbox") != "") {
+ percentCompleteInteger =
+ parseInt(getElementValue("percent-complete-textbox"), 10);
+ }
+ if (percentCompleteInteger < 0) {
+ percentCompleteInteger = 0;
+ } else if (percentCompleteInteger > 100) {
+ percentCompleteInteger = 100;
+ }
+ setItemProperty(item, "PERCENT-COMPLETE", percentCompleteInteger);
+ }
+
+ // Categories
+ saveCategories(item);
+
+ // Attachment
+ // We want the attachments to be up to date, remove all first.
+ item.removeAllAttachments();
+
+ // Now add back the new ones
+ for (let hashId in gAttachMap) {
+ let att = gAttachMap[hashId];
+ item.addAttachment(att);
+ }
+
+ // Description
+ setItemProperty(item, "DESCRIPTION", getElementValue("item-description"));
+
+ // Event Status
+ if (isEvent(item)) {
+ if (gConfig.status && gConfig.status != "NONE") {
+ item.setProperty("STATUS", gConfig.status);
+ } else {
+ item.deleteProperty("STATUS");
+ }
+ } else {
+ let status = getElementValue("todo-status");
+ if (status != "COMPLETED") {
+ item.completedDate = null;
+ }
+ setItemProperty(item, "STATUS", status == "NONE" ? null : status);
+ }
+
+ // set the "PRIORITY" property if a valid priority has been
+ // specified (any integer value except *null*) OR the item
+ // already specifies a priority. in any other case we don't
+ // need this property and can safely delete it. we need this special
+ // handling since the WCAP provider always includes the priority
+ // with value *null* and we don't detect changes to this item if
+ // we delete this property.
+ if (capSupported("priority") &&
+ (gConfig.priority || item.hasProperty("PRIORITY"))) {
+ item.setProperty("PRIORITY", gConfig.priority);
+ } else {
+ item.deleteProperty("PRIORITY");
+ }
+
+ // Transparency
+ if (gConfig.showTimeAs) {
+ item.setProperty("TRANSP", gConfig.showTimeAs);
+ } else {
+ item.deleteProperty("TRANSP");
+ }
+
+ // Privacy
+ setItemProperty(item, "CLASS", gConfig.privacy, "privacy");
+
+ if (item.status == "COMPLETED" && isToDo(item)) {
+ let elementValue = getElementValue("completed-date-picker");
+ item.completedDate = cal.jsDateToDateTime(elementValue);
+ }
+
+ saveReminder(item);
+}
+
+/**
+ * Save date and time related values from the dialog to the passed item.
+ *
+ * @param item The item to save to.
+ */
+function saveDateTime(item) {
+ // Changes to the start date don't have to change the until date.
+ untilDateCompensation(item);
+
+ if (isEvent(item)) {
+ let startTime = gStartTime.getInTimezone(gStartTimezone);
+ let endTime = gEndTime.getInTimezone(gEndTimezone);
+ let isAllDay = getElementValue("event-all-day", "checked");
+ if (isAllDay) {
+ startTime = startTime.clone();
+ endTime = endTime.clone();
+ startTime.isDate = true;
+ endTime.isDate = true;
+ endTime.day += 1;
+ } else {
+ startTime = startTime.clone();
+ startTime.isDate = false;
+ endTime = endTime.clone();
+ endTime.isDate = false;
+ }
+ setItemProperty(item, "startDate", startTime);
+ setItemProperty(item, "endDate", endTime);
+ }
+ if (isToDo(item)) {
+ let startTime = gStartTime && gStartTime.getInTimezone(gStartTimezone);
+ let endTime = gEndTime && gEndTime.getInTimezone(gEndTimezone);
+ setItemProperty(item, "entryDate", startTime);
+ setItemProperty(item, "dueDate", endTime);
+ }
+}
+
+/**
+ * Changes the until date in the rule in order to compensate the automatic
+ * correction caused by the function onStartDateChange() when saving the
+ * item.
+ * It allows to keep the until date set in the dialog irrespective of the
+ * changes that the user has done to the start date.
+ */
+function untilDateCompensation(aItem) {
+ // The current start date in the item is always the date that we get
+ // when opening the dialog or after the last save.
+ let startDate = aItem[calGetStartDateProp(aItem)];
+
+ if (aItem.recurrenceInfo) {
+ let rrules = splitRecurrenceRules(aItem.recurrenceInfo);
+ let rule = rrules[0][0];
+ if (!rule.isByCount && rule.isFinite && startDate) {
+ let compensation = startDate.subtractDate(gStartTime);
+ if (compensation != "PT0S") {
+ let untilDate = rule.untilDate.clone();
+ untilDate.addDuration(compensation);
+ rule.untilDate = untilDate;
+ }
+ }
+ }
+}
+
+/**
+ * Updates the dialog title based on item type and if the item is new or to be
+ * modified.
+ */
+function updateTitle() {
+ let strName;
+ if (cal.isEvent(window.calendarItem)) {
+ strName = (window.mode == "new" ? "newEventDialog" : "editEventDialog");
+ } else if (cal.isToDo(window.calendarItem)) {
+ strName = (window.mode == "new" ? "newTaskDialog" : "editTaskDialog");
+ } else {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ }
+ let newTitle = cal.calGetString("calendar", strName) + ": " +
+ getElementValue("item-title");
+ sendMessage({ command: "updateTitle", argument: newTitle });
+}
+
+/**
+ * Update the disabled status of the accept button. The button is enabled if all
+ * parts of the dialog have options selected that make sense.
+ * constraining factors like
+ */
+function updateAccept() {
+ let enableAccept = true;
+ let kDefaultTimezone = calendarDefaultTimezone();
+ let startDate;
+ let endDate;
+ let isEvent = cal.isEvent(window.calendarItem);
+
+ // don't allow for end dates to be before start dates
+ if (isEvent) {
+ startDate = cal.jsDateToDateTime(getElementValue("event-starttime"));
+ endDate = cal.jsDateToDateTime(getElementValue("event-endtime"));
+ } else {
+ startDate = getElementValue("todo-has-entrydate", "checked") ?
+ cal.jsDateToDateTime(getElementValue("todo-entrydate")) : null;
+ endDate = getElementValue("todo-has-duedate", "checked") ?
+ cal.jsDateToDateTime(getElementValue("todo-duedate")) : null;
+ }
+
+ if (startDate && endDate) {
+ if (gTimezonesEnabled) {
+ let startTimezone = gStartTimezone;
+ let endTimezone = gEndTimezone;
+ if (endTimezone.isUTC) {
+ if (!compareObjects(gStartTimezone, gEndTimezone)) {
+ endTimezone = gStartTimezone;
+ }
+ }
+
+ startDate = startDate.getInTimezone(kDefaultTimezone);
+ endDate = endDate.getInTimezone(kDefaultTimezone);
+
+ startDate.timezone = startTimezone;
+ endDate.timezone = endTimezone;
+ }
+
+ startDate = startDate.getInTimezone(kDefaultTimezone);
+ endDate = endDate.getInTimezone(kDefaultTimezone);
+
+ // For all-day events we are not interested in times and compare only
+ // dates.
+ if (isEvent && getElementValue("event-all-day", "checked")) {
+ // jsDateToDateTime returnes the values in UTC. Depending on the
+ // local timezone and the values selected in datetimepicker the date
+ // in UTC might be shifted to the previous or next day.
+ // For example: The user (with local timezone GMT+05) selected
+ // Feb 10 2006 00:00:00. The corresponding value in UTC is
+ // Feb 09 2006 19:00:00. If we now set isDate to true we end up with
+ // a date of Feb 09 2006 instead of Feb 10 2006 resulting in errors
+ // during the following comparison.
+ // Calling getInTimezone() ensures that we use the same dates as
+ // displayed to the user in datetimepicker for comparison.
+ startDate.isDate = true;
+ endDate.isDate = true;
+ }
+ }
+
+ if (endDate && startDate && endDate.compare(startDate) == -1) {
+ enableAccept = false;
+ }
+
+ enableAcceptCommand(enableAccept);
+
+ return enableAccept;
+}
+
+/**
+ * Enables/disables the commands cmd_accept and cmd_save related to the
+ * save operation.
+ *
+ * @param aEnable true: enables the command
+ */
+function enableAcceptCommand(aEnable) {
+ sendMessage({ command: "enableAcceptCommand", argument: aEnable });
+}
+
+// Global variables used to restore start and end date-time when changing the
+// "all day" status in the onUpdateAllday() function.
+var gOldStartTime = null;
+var gOldEndTime = null;
+var gOldStartTimezone = null;
+var gOldEndTimezone = null;
+
+/**
+ * Handler function to update controls and state in consequence of the "all
+ * day" checkbox being clicked.
+ */
+function onUpdateAllDay() {
+ if (!isEvent(window.calendarItem)) {
+ return;
+ }
+ let allDay = getElementValue("event-all-day", "checked");
+ let kDefaultTimezone = calendarDefaultTimezone();
+
+ if (allDay) {
+ // Store date-times and related timezones so we can restore
+ // if the user unchecks the "all day" checkbox.
+ gOldStartTime = gStartTime.clone();
+ gOldEndTime = gEndTime.clone();
+ gOldStartTimezone = gStartTimezone;
+ gOldEndTimezone = gEndTimezone;
+ // When events that end at 0:00 become all-day events, we need to
+ // subtract a day from the end date because the real end is midnight.
+ if (gEndTime.hour == 0 && gEndTime.minute == 0) {
+ let tempStartTime = gStartTime.clone();
+ let tempEndTime = gEndTime.clone();
+ tempStartTime.isDate = true;
+ tempEndTime.isDate = true;
+ tempStartTime.day++;
+ if (tempEndTime.compare(tempStartTime) >= 0) {
+ gEndTime.day--;
+ }
+ }
+ } else {
+ gStartTime.isDate = false;
+ gEndTime.isDate = false;
+ if (!gOldStartTime && !gOldEndTime) {
+ // The checkbox has been unchecked for the first time, the event
+ // was an "All day" type, so we have to set default values.
+ gStartTime.hour = getDefaultStartDate(window.initialStartDateValue).hour;
+ gEndTime.hour = gStartTime.hour;
+ gEndTime.minute += Preferences.get("calendar.event.defaultlength", 60);
+ gOldStartTimezone = kDefaultTimezone;
+ gOldEndTimezone = kDefaultTimezone;
+ } else {
+ // Restore date-times previously stored.
+ gStartTime.hour = gOldStartTime.hour;
+ gStartTime.minute = gOldStartTime.minute;
+ gEndTime.hour = gOldEndTime.hour;
+ gEndTime.minute = gOldEndTime.minute;
+ // When we restore 0:00 as end time, we need to add one day to
+ // the end date in order to include the last day until midnight.
+ if (gEndTime.hour == 0 && gEndTime.minute == 0) {
+ gEndTime.day++;
+ }
+ }
+ }
+ gStartTimezone = (allDay ? cal.floating() : gOldStartTimezone);
+ gEndTimezone = (allDay ? cal.floating() : gOldEndTimezone);
+ setShowTimeAs(allDay);
+
+ updateAllDay();
+}
+
+/**
+ * This function sets the enabled/disabled state of the following controls:
+ * - 'event-starttime'
+ * - 'event-endtime'
+ * - 'timezone-starttime'
+ * - 'timezone-endtime'
+ * the state depends on whether or not the event is configured as 'all-day' or not.
+ */
+function updateAllDay() {
+ if (gIgnoreUpdate) {
+ return;
+ }
+
+ if (!isEvent(window.calendarItem)) {
+ return;
+ }
+
+ let allDay = getElementValue("event-all-day", "checked");
+ setElementValue("event-starttime", allDay, "timepickerdisabled");
+ setElementValue("event-endtime", allDay, "timepickerdisabled");
+
+ gStartTime.isDate = allDay;
+ gEndTime.isDate = allDay;
+ gItemDuration = gEndTime.subtractDate(gStartTime);
+
+ updateDateTime();
+ updateUntildateRecRule();
+ updateRepeatDetails();
+ updateAccept();
+}
+
+/**
+ * Use the window arguments to cause the opener to create a new event on the
+ * item's calendar
+ */
+function openNewEvent() {
+ let item = window.calendarItem;
+ let args = window.arguments[0];
+ args.onNewEvent(item.calendar);
+}
+
+/**
+ * Use the window arguments to cause the opener to create a new event on the
+ * item's calendar
+ */
+function openNewTask() {
+ let item = window.calendarItem;
+ let args = window.arguments[0];
+ args.onNewTodo(item.calendar);
+}
+
+/**
+ * Update the transparency status of this dialog, depending on if the event
+ * is all-day or not.
+ *
+ * @param allDay If true, the event is all-day
+ */
+function setShowTimeAs(allDay) {
+ gConfig.showTimeAs = cal.getEventDefaultTransparency(allDay);
+ updateConfigState({ showTimeAs: gConfig.showTimeAs });
+}
+
+function editAttendees() {
+ let savedWindow = window;
+ let calendar = getCurrentCalendar();
+
+ let callback = function(attendees, organizer, startTime, endTime) {
+ savedWindow.attendees = attendees;
+ if (organizer) {
+ // In case we didn't have an organizer object before we
+ // added attendees to our event we take the one created
+ // by the 'invite attendee'-dialog.
+ if (savedWindow.organizer) {
+ // The other case is that we already had an organizer object
+ // before we went throught the 'invite attendee'-dialog. In that
+ // case make sure we don't carry over attributes that have been
+ // set to their default values by the dialog but don't actually
+ // exist in the original organizer object.
+ if (!savedWindow.organizer.id) {
+ organizer.id = null;
+ }
+ if (!savedWindow.organizer.role) {
+ organizer.role = null;
+ }
+ if (!savedWindow.organizer.participationStatus) {
+ organizer.participationStatus = null;
+ }
+ if (!savedWindow.organizer.commonName) {
+ organizer.commonName = null;
+ }
+ }
+ savedWindow.organizer = organizer;
+ }
+ let duration = endTime.subtractDate(startTime);
+ startTime = startTime.clone();
+ endTime = endTime.clone();
+ let kDefaultTimezone = calendarDefaultTimezone();
+ gStartTimezone = startTime.timezone;
+ gEndTimezone = endTime.timezone;
+ gStartTime = startTime.getInTimezone(kDefaultTimezone);
+ gEndTime = endTime.getInTimezone(kDefaultTimezone);
+ gItemDuration = duration;
+ updateAttendees();
+ updateDateTime();
+ updateAllDay();
+
+ if (isAllDay != gStartTime.isDate) {
+ setShowTimeAs(gStartTime.isDate);
+ }
+ };
+
+ let startTime = gStartTime.getInTimezone(gStartTimezone);
+ let endTime = gEndTime.getInTimezone(gEndTimezone);
+
+ let isAllDay = getElementValue("event-all-day", "checked");
+ if (isAllDay) {
+ startTime.isDate = true;
+ endTime.isDate = true;
+ endTime.day += 1;
+ } else {
+ startTime.isDate = false;
+ endTime.isDate = false;
+ }
+ let args = {};
+ args.startTime = startTime;
+ args.endTime = endTime;
+ args.displayTimezone = gTimezonesEnabled;
+ args.attendees = window.attendees;
+ args.organizer = window.organizer && window.organizer.clone();
+ args.calendar = calendar;
+ args.item = window.calendarItem;
+ args.onOk = callback;
+ args.fbWrapper = window.fbWrapper;
+
+ // open the dialog modally
+ openDialog(
+ "chrome://calendar/content/calendar-event-dialog-attendees.xul",
+ "_blank",
+ "chrome,titlebar,modal,resizable",
+ args);
+}
+
+/**
+ * Updates the UI outside of the iframe (toolbar, menu, statusbar, etc.)
+ * for changes in priority, privacy, status, showTimeAs/transparency,
+ * and/or other properties. This function should be called any time that
+ * gConfig.privacy, gConfig.priority, etc. are updated.
+ *
+ * Privacy and priority updates depend on the selected calendar. If the
+ * selected calendar does not support them, or only supports certain
+ * values, these are removed from the UI.
+ *
+ * @param {Object} aArg Container
+ * @param {string} aArg.privacy (optional) The new privacy value
+ * @param {short} aArg.priority (optional) The new priority value
+ * @param {string} aArg.status (optional) The new status value
+ * @param {string} aArg.showTimeAs (optional) The new transparency value
+ */
+function updateConfigState(aArg) {
+ // We include additional info for priority and privacy.
+ if (aArg.hasOwnProperty("priority")) {
+ aArg.hasPriority = capSupported("priority");
+ }
+ if (aArg.hasOwnProperty("privacy")) {
+ Object.assign(aArg, {
+ hasPrivacy: capSupported("privacy"),
+ calendarType: getCurrentCalendar().type,
+ privacyValues: capValues("privacy",
+ ["PUBLIC", "CONFIDENTIAL", "PRIVATE"])
+ });
+ }
+
+ // For tasks, do not include showTimeAs
+ if (aArg.hasOwnProperty("showTimeAs") && cal.isToDo(window.calendarItem)) {
+ delete aArg.showTimeAs;
+ if (Object.keys(aArg).length == 0) {
+ return;
+ }
+ }
+
+ sendMessage({ command: "updateConfigState", argument: aArg });
+}
+
+/**
+ * Add menu items to the UI for attaching files using cloud providers.
+ */
+function loadCloudProviders() {
+ let cloudFileEnabled = Preferences.get("mail.cloud_files.enabled", false);
+ let cmd = document.getElementById("cmd_attach_cloud");
+ let message = {
+ command: "setElementAttribute",
+ argument: { id: "cmd_attach_cloud", attribute: "hidden", value: null }
+ };
+
+ if (!cloudFileEnabled) {
+ // If cloud file support is disabled, just hide the attach item
+ cmd.hidden = true;
+ message.argument.value = true;
+ sendMessage(message);
+ return;
+ }
+
+ let isHidden = cloudFileAccounts.accounts.length == 0;
+ cmd.hidden = isHidden;
+ message.argument.value = isHidden;
+ sendMessage(message);
+
+ let itemObjects = [];
+
+ for (let cloudProvider of cloudFileAccounts.accounts) {
+ // Create a serializable object to pass in a message outside the iframe
+ let itemObject = {};
+ itemObject.displayName = cloudFileAccounts.getDisplayName(cloudProvider);
+ itemObject.label = cal.calGetString("calendar-event-dialog", "attachViaFilelink", [itemObject.displayName]);
+ itemObject.cloudProviderAccountKey = cloudProvider.accountKey;
+ if (cloudProvider.iconClass) {
+ itemObject.class = "menuitem-iconic";
+ itemObject.image = cloudProvider.iconClass;
+ }
+
+ itemObjects.push(itemObject);
+
+ // Create a menu item from the serializable object
+ let item = createXULElement("menuitem");
+ item.setAttribute("label", itemObject.label);
+ item.setAttribute("observes", "cmd_attach_cloud");
+ item.setAttribute("oncommand", "attachFile(event.target.cloudProvider); event.stopPropagation();");
+
+ if (itemObject.class) {
+ item.setAttribute("class", itemObject.class);
+ item.setAttribute("image", itemObject.image);
+ }
+
+ // Add the menu item to places inside the iframe where we advertise cloud providers
+ let attachmentPopup = document.getElementById("attachment-popup");
+ attachmentPopup.appendChild(item).cloudProvider = cloudProvider;
+ }
+
+ // Add the items to places outside the iframe where we advertise cloud providers
+ sendMessage({ command: "loadCloudProviders", items: itemObjects });
+}
+
+/**
+ * Prompts the user to attach an url to this item.
+ */
+function attachURL() {
+ if (Services.prompt) {
+ // ghost in an example...
+ let result = { value: "http://" };
+ if (Services.prompt.prompt(window,
+ calGetString("calendar-event-dialog",
+ "specifyLinkLocation"),
+ calGetString("calendar-event-dialog",
+ "enterLinkLocation"),
+ result,
+ null,
+ { value: 0 })) {
+ try {
+ // If something bogus was entered, makeURL may fail.
+ let attachment = createAttachment();
+ attachment.uri = makeURL(result.value);
+ addAttachment(attachment);
+ } catch (e) {
+ // TODO We might want to show a warning instead of just not
+ // adding the file
+ }
+ }
+ }
+}
+
+/**
+ * Attach a file using a cloud provider, identified by its accountKey.
+ *
+ * @param {string} aAccountKey The accountKey for a cloud provider
+ */
+function attachFileByAccountKey(aAccountKey) {
+ for (let cloudProvider of cloudFileAccounts.accounts) {
+ if (aAccountKey == cloudProvider.accountKey) {
+ attachFile(cloudProvider);
+ return;
+ }
+ }
+}
+
+/**
+ * Attach a file to the item. Not passing a cloud provider is currently unsupported.
+ *
+ * @param cloudProvider If set, the cloud provider will be used for attaching
+ */
+function attachFile(cloudProvider) {
+ if (!cloudProvider) {
+ cal.ERROR("[calendar-event-dialog] Could not attach file without cloud provider" + cal.STACK(10));
+ }
+
+ let files;
+ try {
+ const nsIFilePicker = Components.interfaces.nsIFilePicker;
+ let filePicker = Components.classes["@mozilla.org/filepicker;1"]
+ .createInstance(nsIFilePicker);
+ filePicker.init(window,
+ calGetString("calendar-event-dialog", "selectAFile"),
+ nsIFilePicker.modeOpenMultiple);
+
+ // Check for the last directory
+ let lastDir = lastDirectory();
+ if (lastDir) {
+ filePicker.displayDirectory = lastDir;
+ }
+
+ // Get the attachment
+ if (filePicker.show() == nsIFilePicker.returnOK) {
+ files = filePicker.files;
+ }
+ } catch (ex) {
+ dump("failed to get attachments: " +ex+ "\n");
+ }
+
+ // Check if something has to be done
+ if (!files || !files.hasMoreElements()) {
+ return;
+ }
+
+ // Create the attachment
+ while (files.hasMoreElements()) {
+ let file = files.getNext().QueryInterface(Components.interfaces.nsILocalFile);
+
+ let fileHandler = Services.io.getProtocolHandler("file")
+ .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
+ let uriSpec = fileHandler.getURLSpecFromFile(file);
+
+ if (!(uriSpec in gAttachMap)) {
+ // If the attachment hasn't been added, then set the last display
+ // directory.
+ lastDirectory(uriSpec);
+
+ // ... and add the attachment.
+ let attachment = cal.createAttachment();
+ if (cloudProvider) {
+ attachment.uri = makeURL(uriSpec);
+ } else {
+ // TODO read file into attachment
+ }
+ addAttachment(attachment, cloudProvider);
+ }
+ }
+}
+
+/**
+ * Helper function to remember the last directory chosen when attaching files.
+ *
+ * @param aFileUri (optional) If passed, the last directory will be set and
+ * returned. If null, the last chosen directory
+ * will be returned.
+ * @return The last directory that was set with this function.
+ */
+function lastDirectory(aFileUri) {
+ if (aFileUri) {
+ // Act similar to a setter, save the passed uri.
+ let uri = makeURL(aFileUri);
+ let file = uri.QueryInterface(Components.interfaces.nsIFileURL).file;
+ lastDirectory.mValue = file.parent.QueryInterface(Components.interfaces.nsILocalFile);
+ }
+
+ // In any case, return the value
+ return (lastDirectory.mValue === undefined ? null : lastDirectory.mValue);
+}
+
+/**
+ * Turns an url into a string that can be used in UI.
+ * - For a file:// url, shows the filename.
+ * - For a http:// url, removes protocol and trailing slash
+ *
+ * @param aUri The uri to parse.
+ * @return A string that can be used in UI.
+ */
+function makePrettyName(aUri) {
+ let name = aUri.spec;
+
+ if (aUri.schemeIs("file")) {
+ name = aUri.spec.split("/").pop();
+ } else if (aUri.schemeIs("http")) {
+ name = aUri.spec.replace(/\/$/, "").replace(/^http:\/\//, "");
+ }
+ return name;
+}
+
+/**
+ * Asynchronously uploads the given attachment to the cloud provider, updating
+ * the passed listItem as things progress.
+ *
+ * @param attachment A calIAttachment to upload
+ * @param cloudProvider The clould provider to upload to
+ * @param listItem The listitem in attachment-link listbox to update.
+ */
+function uploadCloudAttachment(attachment, cloudProvider, listItem) {
+ let file = attachment.uri.QueryInterface(Components.interfaces.nsIFileURL).file;
+ listItem.attachLocalFile = file;
+ listItem.attachCloudProvider = cloudProvider;
+ cloudProvider.uploadFile(file, {
+ onStartRequest: function() {
+ listItem.setAttribute("image", "chrome://global/skin/icons/loading.png");
+ },
+
+ onStopRequest: function(aRequest, aContext, aStatusCode) {
+ if (Components.isSuccessCode(aStatusCode)) {
+ delete gAttachMap[attachment.hashId];
+ attachment.uri = makeURL(cloudProvider.urlForFile(file));
+ attachment.setParameter("FILENAME", file.leafName);
+ attachment.setParameter("PROVIDER", cloudProvider.type);
+ listItem.setAttribute("label", file.leafName);
+ gAttachMap[attachment.hashId] = attachment;
+ listItem.setAttribute("image", cloudProvider.iconClass);
+ updateAttachment();
+ } else {
+ cal.ERROR("[calendar-event-dialog] Uploading cloud attachment " +
+ "failed. Status code: " + aStatusCode);
+
+ // Uploading failed. First of all, show an error icon. Also,
+ // delete it from the attach map now, this will make sure it is
+ // not serialized if the user saves.
+ listItem.setAttribute("image", "chrome://messenger/skin/icons/error.png");
+ delete gAttachMap[attachment.hashId];
+
+ // Keep the item for a while so the user can see something failed.
+ // When we have a nice notification bar, we can show more info
+ // about the failure.
+ setTimeout(() => {
+ listItem.remove();
+ updateAttachment();
+ }, 5000);
+ }
+ }
+ });
+}
+
+/**
+ * Adds the given attachment to dialog controls.
+ *
+ * @param attachment The calIAttachment object to add
+ * @param cloudProvider (optional) If set, the given cloud provider will be used.
+ */
+function addAttachment(attachment, cloudProvider) {
+ if (!attachment ||
+ !attachment.hashId ||
+ attachment.hashId in gAttachMap) {
+ return;
+ }
+
+ // We currently only support uri attachments
+ if (attachment.uri) {
+ let documentLink = document.getElementById("attachment-link");
+ let listItem = createXULElement("listitem");
+
+ // Set listitem attributes
+ listItem.setAttribute("label", makePrettyName(attachment.uri));
+ listItem.setAttribute("crop", "end");
+ listItem.setAttribute("class", "listitem-iconic");
+ listItem.setAttribute("tooltiptext", attachment.uri.spec);
+ if (cloudProvider) {
+ if (attachment.uri.schemeIs("file")) {
+ // Its still a local url, needs to be uploaded
+ listItem.setAttribute("image", "chrome://messenger/skin/icons/connecting.png");
+ uploadCloudAttachment(attachment, cloudProvider, listItem);
+ } else {
+ let leafName = attachment.getParameter("FILENAME");
+ listItem.setAttribute("image", cloudProvider.iconClass);
+ if (leafName) {
+ listItem.setAttribute("label", leafName);
+ }
+ }
+ } else if (attachment.uri.schemeIs("file")) {
+ listItem.setAttribute("image", "moz-icon://" + attachment.uri);
+ } else {
+ let leafName = attachment.getParameter("FILENAME");
+ let providerType = attachment.getParameter("PROVIDER");
+ let cloudFileEnabled = Preferences.get("mail.cloud_files.enabled", false);
+
+ if (leafName) {
+ // TODO security issues?
+ listItem.setAttribute("label", leafName);
+ }
+ if (providerType && cloudFileEnabled) {
+ let provider = cloudFileAccounts.getProviderForType(providerType);
+ listItem.setAttribute("image", provider.iconClass);
+ } else {
+ listItem.setAttribute("image", "moz-icon://dummy.html");
+ }
+ }
+
+ // Now that everything is set up, add it to the attachment box.
+ documentLink.appendChild(listItem);
+
+ // full attachment object is stored here
+ listItem.attachment = attachment;
+
+ // Update the number of rows and save our attachment globally
+ documentLink.rows = documentLink.getRowCount();
+ }
+
+ gAttachMap[attachment.hashId] = attachment;
+ updateAttachment();
+}
+
+/**
+ * Removes the currently selected attachment from the dialog controls.
+ *
+ * XXX This could use a dialog maybe?
+ */
+function deleteAttachment() {
+ let documentLink = document.getElementById("attachment-link");
+ let item = documentLink.selectedItem;
+ delete gAttachMap[item.attachment.hashId];
+ documentLink.removeItemAt(documentLink.selectedIndex);
+
+ if (item.attachLocalFile && item.attachCloudProvider) {
+ try {
+ item.attachCloudProvider.deleteFile(item.attachLocalFile, {
+ onStartRequest: function() {},
+ onStopRequest: function(aRequest, aContext, aStatusCode) {
+ if (!Components.isSuccessCode(aStatusCode)) {
+ // TODO With a notification bar, we could actually show this error.
+ cal.ERROR("[calendar-event-dialog] Deleting cloud attachment " +
+ "failed, file will remain on server. " +
+ " Status code: " + aStatusCode);
+ }
+ }
+ });
+ } catch (e) {
+ cal.ERROR("[calendar-event-dialog] Deleting cloud attachment " +
+ "failed, file will remain on server. " +
+ "Exception: " + e);
+ }
+ }
+
+ updateAttachment();
+}
+
+/**
+ * Removes all attachments from the dialog controls.
+ */
+function deleteAllAttachments() {
+ let documentLink = document.getElementById("attachment-link");
+ let itemCount = documentLink.getRowCount();
+ let canRemove = (itemCount < 2);
+
+ if (itemCount > 1) {
+ let removeText = PluralForm.get(itemCount, cal.calGetString("calendar-event-dialog", "removeAttachmentsText"));
+ let removeTitle = cal.calGetString("calendar-event-dialog", "removeCalendarsTitle");
+ canRemove = Services.prompt.confirm(window, removeTitle, removeText.replace("#1", itemCount), {});
+ }
+
+ if (canRemove) {
+ let child;
+ while (documentLink.hasChildNodes()) {
+ child = documentLink.lastChild;
+ child.attachment = null;
+ child.remove();
+ }
+ gAttachMap = {};
+ }
+ updateAttachment();
+}
+
+/**
+ * Opens the selected attachment using the external protocol service.
+ * @see nsIExternalProtocolService
+ */
+function openAttachment() {
+ // Only one file has to be selected and we don't handle base64 files at all
+ let documentLink = document.getElementById("attachment-link");
+ if (documentLink.selectedItems.length == 1) {
+ let attURI = documentLink.getSelectedItem(0).attachment.uri;
+ let externalLoader = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Components.interfaces.nsIExternalProtocolService);
+ // TODO There should be a nicer dialog
+ externalLoader.loadUrl(attURI);
+ }
+}
+
+/**
+ * Copies the link location of the first selected attachment to the clipboard
+ */
+function copyAttachment() {
+ let documentLink = document.getElementById("attachment-link");
+ let attURI = documentLink.getSelectedItem(0).attachment.uri.spec;
+ let clipboard = Components.classes["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Components.interfaces.nsIClipboardHelper);
+ clipboard.copyString(attURI);
+}
+
+/**
+ * Handler function to handle pressing keys in the attachment listbox.
+ *
+ * @param aEvent The DOM event caused by the key press.
+ */
+function attachmentLinkKeyPress(aEvent) {
+ const kKE = Components.interfaces.nsIDOMKeyEvent;
+ switch (aEvent.keyCode) {
+ case kKE.DOM_VK_BACK_SPACE:
+ case kKE.DOM_VK_DELETE:
+ deleteAttachment();
+ break;
+ case kKE.DOM_VK_RETURN:
+ openAttachment();
+ break;
+ }
+}
+
+/**
+ * Handler function to take care of double clicking on an attachment
+ *
+ * @param aEvent The DOM event caused by the clicking.
+ */
+function attachmentDblClick(aEvent) {
+ // left double click on a list item
+ if (aEvent.originalTarget.localName == "listitem" && aEvent.button == 0) {
+ openAttachment();
+ }
+}
+
+/**
+ * Handler function to take care of right clicking on an attachment or the attachment list
+ *
+ * @param aEvent The DOM event caused by the clicking.
+ */
+function attachmentClick(aEvent) {
+ // we take only care about right clicks
+ if (aEvent.button != 2) {
+ return;
+ }
+ let attachmentPopup = document.getElementById("attachment-popup");
+ for (let node of attachmentPopup.childNodes) {
+ if (aEvent.originalTarget.localName == "listitem" ||
+ node.id == "attachment-popup-attachPage") {
+ showElement(node);
+ } else {
+ hideElement(node);
+ }
+ }
+}
+
+/**
+ * Helper function to show a notification in the event-dialog's notificationBox
+ *
+ * @param aMessage the message text to show
+ * @param aValue string identifying the notification
+ * @param aPriority (optional) the priority of the warning (info, critical), default is 'warn'
+ * @param aImage (optional) URL of image to appear on the notification
+ * @param aButtonset (optional) array of button descriptions to appear on the notification
+ * @param aCallback (optional) a function to handle events from the notificationBox
+ */
+function notifyUser(aMessage, aValue, aPriority, aImage, aButtonset, aCallback) {
+ let notificationBox = document.getElementById("event-dialog-notifications");
+ // only append, if the notification does not already exist
+ if (notificationBox.getNotificationWithValue(aValue) == null) {
+ const prioMap = {
+ info: notificationBox.PRIORITY_INFO_MEDIUM,
+ critical: notificationBox.PRIORITY_CRITICAL_MEDIUM
+ };
+ let priority = prioMap[aPriority] || notificationBox.PRIORITY_WARNING_MEDIUM;
+ notificationBox.appendNotification(aMessage,
+ aValue,
+ aImage,
+ priority,
+ aButtonset,
+ aCallback);
+ }
+}
+
+/**
+ * Remove a notification from the notifiactionBox
+ *
+ * @param aValue string identifying the notification to remove
+ */
+function removeNotification(aValue) {
+ let notificationBox = document.getElementById("event-dialog-notifications");
+ let notification = notificationBox.getNotificationWithValue(aValue);
+ if (notification != null) {
+ notificationBox.removeNotification(notification);
+ }
+}
+
+/**
+ * Update the dialog controls related to the item's calendar.
+ */
+function updateCalendar() {
+ let item = window.calendarItem;
+ let calendar = getCurrentCalendar();
+
+ gIsReadOnly = calendar.readOnly;
+
+ if (!gPreviousCalendarId) {
+ gPreviousCalendarId = item.calendar.id;
+ }
+
+ // We might have to change the organizer, let's see
+ let calendarOrgId = calendar.getProperty("organizerId");
+ if (window.organizer && calendarOrgId &&
+ calendar.id != gPreviousCalendarId) {
+ window.organizer.id = calendarOrgId;
+ window.organizer.commonName = calendar.getProperty("organizerCN");
+ gPreviousCalendarId = calendar.id;
+ }
+
+ if (!canNotifyAttendees(calendar, item) && calendar.getProperty("imip.identity")) {
+ enableElement("notify-attendees-checkbox");
+ enableElement("undisclose-attendees-checkbox");
+ } else {
+ disableElement("notify-attendees-checkbox");
+ disableElement("undisclose-attendees-checkbox");
+ }
+
+ // update the accept button
+ updateAccept();
+
+ // TODO: the code above decided about whether or not the item is readonly.
+ // below we enable/disable all controls based on this decision.
+ // unfortunately some controls need to be disabled based on some other
+ // criteria. this is why we enable all controls in case the item is *not*
+ // readonly and run through all those updateXXX() functions to disable
+ // them again based on the specific logic build into those function. is this
+ // really a good idea?
+ if (gIsReadOnly) {
+ let disableElements = document.getElementsByAttribute("disable-on-readonly", "true");
+ for (let element of disableElements) {
+ element.setAttribute("disabled", "true");
+
+ // we mark link-labels with the hyperlink attribute, since we need
+ // to remove their class in case they get disabled. TODO: it would
+ // be better to create a small binding for those link-labels
+ // instead of adding those special stuff.
+ if (element.hasAttribute("hyperlink")) {
+ element.removeAttribute("class");
+ element.removeAttribute("onclick");
+ }
+ }
+
+ let collapseElements = document.getElementsByAttribute("collapse-on-readonly", "true");
+ for (let element of collapseElements) {
+ element.setAttribute("collapsed", "true");
+ }
+ } else {
+ sendMessage({ command: "removeDisableAndCollapseOnReadonly" });
+
+ let enableElements = document.getElementsByAttribute("disable-on-readonly", "true");
+ for (let element of enableElements) {
+ element.removeAttribute("disabled");
+ if (element.hasAttribute("hyperlink")) {
+ element.setAttribute("class", "text-link");
+ }
+ }
+
+ let collapseElements = document.getElementsByAttribute("collapse-on-readonly", "true");
+ for (let element of collapseElements) {
+ element.removeAttribute("collapsed");
+ }
+
+ // Task completed date
+ if (item.completedDate) {
+ updateToDoStatus(item.status, cal.dateTimeToJsDate(item.completedDate));
+ } else {
+ updateToDoStatus(item.status);
+ }
+
+ // disable repeat menupopup if this is an occurrence
+ item = window.calendarItem;
+ if (item.parentItem != item) {
+ disableElement("item-repeat");
+ disableElement("repeat-until-datepicker");
+ let repeatDetails = document.getElementById("repeat-details");
+ let numChilds = repeatDetails.childNodes.length;
+ for (let i = 0; i < numChilds; i++) {
+ let node = repeatDetails.childNodes[i];
+ node.setAttribute("disabled", "true");
+ node.removeAttribute("class");
+ node.removeAttribute("onclick");
+ }
+ }
+
+ // If the item is a proxy occurrence/instance, a few things aren't
+ // valid.
+ if (item.parentItem != item) {
+ disableElement("item-calendar");
+
+ // don't allow to revoke the entrydate of recurring todo's.
+ disableElementWithLock("todo-has-entrydate", "permanent-lock");
+ }
+
+ // update datetime pickers, disable checkboxes if dates are required by
+ // recurrence or reminders.
+ updateRepeat(true);
+ updateReminder(true);
+ updateAllDay();
+ }
+
+ // Make sure capabilties are reflected correctly
+ updateCapabilities();
+}
+
+/**
+ * Opens the recurrence dialog modally to allow the user to edit the recurrence
+ * rules.
+ */
+function editRepeat() {
+ let args = {};
+ args.calendarEvent = window.calendarItem;
+ args.recurrenceInfo = window.recurrenceInfo;
+ args.startTime = gStartTime;
+ args.endTime = gEndTime;
+
+ let savedWindow = window;
+ args.onOk = function(recurrenceInfo) {
+ savedWindow.recurrenceInfo = recurrenceInfo;
+ };
+
+ window.setCursor("wait");
+
+ // open the dialog modally
+ openDialog(
+ "chrome://calendar/content/calendar-event-dialog-recurrence.xul",
+ "_blank",
+ "chrome,titlebar,modal,resizable",
+ args);
+}
+
+/**
+ * This function is responsible for propagating UI state to controls
+ * depending on the repeat setting of an item. This functionality is used
+ * after the dialog has been loaded as well as if the repeat pattern has
+ * been changed.
+ *
+ * @param aSuppressDialogs If true, controls are updated without prompting
+ * for changes with the recurrence dialog
+ * @param aItemRepeatCall True when the function is being called from
+ * the item-repeat menu list. It allows to detect
+ * a change from the "custom" option.
+ */
+function updateRepeat(aSuppressDialogs, aItemRepeatCall) {
+ function setUpEntrydateForTask(item) {
+ // if this item is a task, we need to make sure that it has
+ // an entry-date, otherwise we can't create a recurrence.
+ if (isToDo(item)) {
+ // automatically check 'has entrydate' if needed.
+ if (!getElementValue("todo-has-entrydate", "checked")) {
+ setElementValue("todo-has-entrydate", "true", "checked");
+
+ // make sure gStartTime is properly initialized
+ updateEntryDate();
+ }
+
+ // disable the checkbox to indicate that we need
+ // the entry-date. the 'disabled' state will be
+ // revoked if the user turns off the repeat pattern.
+ disableElementWithLock("todo-has-entrydate", "repeat-lock");
+ }
+ }
+
+ let repeatMenu = document.getElementById("item-repeat");
+ let repeatValue = repeatMenu.selectedItem.getAttribute("value");
+ let repeatDeck = document.getElementById("repeat-deck");
+
+ if (repeatValue == "none") {
+ repeatDeck.selectedIndex = -1;
+ window.recurrenceInfo = null;
+ let item = window.calendarItem;
+ if (isToDo(item)) {
+ enableElementWithLock("todo-has-entrydate", "repeat-lock");
+ }
+ } else if (repeatValue == "custom") {
+ let lastRepeatDeck = repeatDeck.selectedIndex;
+ repeatDeck.selectedIndex = 1;
+ // the user selected custom repeat pattern. we now need to bring
+ // up the appropriate dialog in order to let the user specify the
+ // new rule. First of all, retrieve the item we want to specify
+ // the custom repeat pattern for.
+ let item = window.calendarItem;
+
+ setUpEntrydateForTask(item);
+
+ // retrieve the current recurrence info, we need this
+ // to find out whether or not the user really created
+ // a new repeat pattern.
+ let recurrenceInfo = window.recurrenceInfo;
+
+ // now bring up the recurrence dialog.
+ // don't pop up the dialog if aSuppressDialogs was specified or if
+ // called during initialization of the dialog.
+ if (!aSuppressDialogs && repeatMenu.hasAttribute("last-value")) {
+ editRepeat();
+ }
+
+ // Assign gUntilDate on the first run or when returning from the
+ // edit recurrence dialog.
+ if (window.recurrenceInfo) {
+ let rrules = splitRecurrenceRules(window.recurrenceInfo);
+ let rule = rrules[0][0];
+ gUntilDate = null;
+ if (!rule.isByCount && rule.isFinite) {
+ gUntilDate = rule.untilDate.clone().getInTimezone(calendarDefaultTimezone());
+ }
+ }
+
+ // we need to address two separate cases here.
+ // 1)- We need to revoke the selection of the repeat
+ // drop down list in case the user didn't specify
+ // a new repeat pattern (i.e. canceled the dialog);
+ // - re-enable the 'has entrydate' option in case
+ // we didn't end up with a recurrence rule.
+ // 2) Check whether the new recurrence rule needs the
+ // recurrence details text or it can be displayed
+ // only with the repeat-until-datepicker.
+ if (recurrenceInfo == window.recurrenceInfo) {
+ repeatMenu.selectedIndex = gLastRepeatSelection;
+ repeatDeck.selectedIndex = lastRepeatDeck;
+ if (isToDo(item)) {
+ if (!window.recurrenceInfo) {
+ enableElementWithLock("todo-has-entrydate", "repeat-lock");
+ }
+ }
+ } else {
+ // From the Edit Recurrence dialog, the rules "every day" and
+ // "every weekday" don't need the recurrence details text when they
+ // have only the until date. The getRepeatTypeAndUntilDate()
+ // function verifies whether this is the case.
+ let [repeatType, untilDate] = getRepeatTypeAndUntilDate(item);
+ if (gNewItemUI) {
+ gTopComponent.importState({
+ repeat: repeatType,
+ repeatUntilDate: untilDate
+ });
+ // XXX more to do, see loadRepeat
+ } else {
+ loadRepeat(repeatType, untilDate, window.calendarItem);
+ }
+ }
+ } else {
+ let item = window.calendarItem;
+ let recurrenceInfo = window.recurrenceInfo || item.recurrenceInfo;
+ let proposedUntilDate = (gStartTime || window.initialStartDateValue).clone();
+
+ if (recurrenceInfo) {
+ recurrenceInfo = recurrenceInfo.clone();
+ let rrules = splitRecurrenceRules(recurrenceInfo);
+ let rule = rrules[0][0];
+
+ // If the previous rule was "custom" we have to recover the until
+ // date, or the last occurrence's date in order to set the
+ // repeat-until-datepicker with the same date.
+ if (aItemRepeatCall && repeatDeck.selectedIndex == 1) {
+ let repeatDate;
+ if (!rule.isByCount || !rule.isFinite) {
+ if (rule.isFinite) {
+ repeatDate = rule.untilDate.getInTimezone(cal.floating());
+ repeatDate = cal.dateTimeToJsDate(repeatDate);
+ } else {
+ repeatDate = "forever";
+ }
+ } else {
+ // Try to recover the last occurrence in 10(?) years.
+ let endDate = gStartTime.clone();
+ endDate.year += 10;
+ let lastOccurrenceDate = null;
+ let dates = recurrenceInfo.getOccurrenceDates(gStartTime, endDate, 0, {});
+ if (dates) {
+ lastOccurrenceDate = dates[dates.length - 1];
+ }
+ repeatDate = (lastOccurrenceDate || proposedUntilDate).getInTimezone(cal.floating());
+ repeatDate = cal.dateTimeToJsDate(repeatDate);
+ }
+ setElementValue("repeat-until-datepicker", repeatDate);
+ }
+ if (rrules[0].length > 0) {
+ recurrenceInfo.deleteRecurrenceItem(rule);
+ }
+ } else {
+ // New event proposes "forever" as default until date.
+ recurrenceInfo = createRecurrenceInfo(item);
+ setElementValue("repeat-until-datepicker", "forever");
+ }
+
+ repeatDeck.selectedIndex = 0;
+
+ let recRule = createRecurrenceRule();
+ recRule.interval = 1;
+ switch (repeatValue) {
+ case "daily":
+ recRule.type = "DAILY";
+ break;
+ case "weekly":
+ recRule.type = "WEEKLY";
+ break;
+ case "every.weekday":
+ recRule.type = "DAILY";
+ recRule.setComponent("BYDAY", 5, [2, 3, 4, 5, 6]);
+ break;
+ case "bi.weekly":
+ recRule.type = "WEEKLY";
+ recRule.interval = 2;
+ break;
+ case "monthly":
+ recRule.type = "MONTHLY";
+ break;
+ case "yearly":
+ recRule.type = "YEARLY";
+ break;
+ }
+
+ setUpEntrydateForTask(item);
+ updateUntildateRecRule(recRule);
+
+ recurrenceInfo.insertRecurrenceItemAt(recRule, 0);
+ window.recurrenceInfo = recurrenceInfo;
+
+ if (isToDo(item)) {
+ if (!getElementValue("todo-has-entrydate", "checked")) {
+ setElementValue("todo-has-entrydate", "true", "checked");
+ }
+ disableElementWithLock("todo-has-entrydate", "repeat-lock");
+ }
+
+ // Preset the until-datepicker's minimonth to the start date.
+ let startDate = cal.dateTimeToJsDate(gStartTime.getInTimezone(cal.floating()));
+ document.getElementById("repeat-until-datepicker").extraDate = startDate;
+ }
+
+ gLastRepeatSelection = repeatMenu.selectedIndex;
+ repeatMenu.setAttribute("last-value", repeatValue);
+
+ updateRepeatDetails();
+ updateEntryDate();
+ updateDueDate();
+ updateAccept();
+}
+
+/**
+ * Update the until date in the recurrence rule in order to set
+ * the same time of the start date.
+ *
+ * @param recRule (optional) The recurrence rule
+ */
+function updateUntildateRecRule(recRule) {
+ if (!recRule) {
+ let recurrenceInfo = window.recurrenceInfo;
+ if (!recurrenceInfo) {
+ return;
+ }
+ let rrules = splitRecurrenceRules(recurrenceInfo);
+ recRule = rrules[0][0];
+ }
+ let defaultTimezone = cal.calendarDefaultTimezone();
+ let repeatUntilDate = null;
+
+ let itemRepeat = document.getElementById("item-repeat").selectedItem.value;
+ if (itemRepeat == "none") {
+ return;
+ } else if (itemRepeat == "custom") {
+ repeatUntilDate = gUntilDate;
+ } else {
+ let untilDatepickerDate = getElementValue("repeat-until-datepicker");
+ if (untilDatepickerDate != "forever") {
+ repeatUntilDate = cal.jsDateToDateTime(untilDatepickerDate, defaultTimezone);
+ }
+ }
+
+ if (repeatUntilDate) {
+ if (onLoad.hasLoaded) {
+ repeatUntilDate.isDate = gStartTime.isDate; // Enforce same value type as DTSTART
+ if (!gStartTime.isDate) {
+ repeatUntilDate.hour = gStartTime.hour;
+ repeatUntilDate.minute = gStartTime.minute;
+ repeatUntilDate.second = gStartTime.second;
+ }
+ }
+ recRule.untilDate = repeatUntilDate.clone();
+ gUntilDate = repeatUntilDate.clone().getInTimezone(defaultTimezone);
+ } else {
+ // Rule that recurs forever or with a "count" number of recurrences.
+ gUntilDate = null;
+ }
+}
+
+/**
+ * Updates the UI controls related to a task's completion status.
+ *
+ * @param {string} aStatus The item's completion status or a string
+ * that allows to identify a change in the
+ * percent-complete's textbox.
+ * @param {Date} aCompletedDate The item's completed date (as a JSDate).
+ */
+function updateToDoStatus(aStatus, aCompletedDate=null) {
+ // RFC2445 doesn't support completedDates without the todo's status
+ // being "COMPLETED", however twiddling the status menulist shouldn't
+ // destroy that information at this point (in case you change status
+ // back to COMPLETED). When we go to store this VTODO as .ics the
+ // date will get lost.
+
+ // remember the original values
+ let oldPercentComplete = parseInt(getElementValue("percent-complete-textbox"), 10);
+ let oldCompletedDate = getElementValue("completed-date-picker");
+
+ // If the percent completed has changed to 100 or from 100 to another
+ // value, the status must change.
+ if (aStatus == "percent-changed") {
+ let selectedIndex = document.getElementById("todo-status").selectedIndex;
+ let menuItemCompleted = selectedIndex == 3;
+ let menuItemNotSpecified = selectedIndex == 0;
+ if (oldPercentComplete == 100) {
+ aStatus = "COMPLETED";
+ } else if (menuItemCompleted || menuItemNotSpecified) {
+ aStatus = "IN-PROCESS";
+ }
+ }
+
+ switch (aStatus) {
+ case null:
+ case "":
+ case "NONE":
+ oldPercentComplete = 0;
+ document.getElementById("todo-status").selectedIndex = 0;
+ disableElement("percent-complete-textbox");
+ disableElement("percent-complete-label");
+ break;
+ case "CANCELLED":
+ document.getElementById("todo-status").selectedIndex = 4;
+ disableElement("percent-complete-textbox");
+ disableElement("percent-complete-label");
+ break;
+ case "COMPLETED":
+ document.getElementById("todo-status").selectedIndex = 3;
+ enableElement("percent-complete-textbox");
+ enableElement("percent-complete-label");
+ // if there is no aCompletedDate, set it to the previous value
+ if (!aCompletedDate) {
+ aCompletedDate = oldCompletedDate;
+ }
+ break;
+ case "IN-PROCESS":
+ document.getElementById("todo-status").selectedIndex = 2;
+ disableElement("completed-date-picker");
+ enableElement("percent-complete-textbox");
+ enableElement("percent-complete-label");
+ break;
+ case "NEEDS-ACTION":
+ document.getElementById("todo-status").selectedIndex = 1;
+ enableElement("percent-complete-textbox");
+ enableElement("percent-complete-label");
+ break;
+ }
+
+ let newPercentComplete;
+ if ((aStatus == "IN-PROCESS" || aStatus == "NEEDS-ACTION") &&
+ oldPercentComplete == 100) {
+ newPercentComplete = 0;
+ setElementValue("completed-date-picker", oldCompletedDate);
+ disableElement("completed-date-picker");
+ } else if (aStatus == "COMPLETED") {
+ newPercentComplete = 100;
+ setElementValue("completed-date-picker", aCompletedDate);
+ enableElement("completed-date-picker");
+ } else {
+ newPercentComplete = oldPercentComplete;
+ setElementValue("completed-date-picker", oldCompletedDate);
+ disableElement("completed-date-picker");
+ }
+
+ gConfig.percentComplete = newPercentComplete;
+ setElementValue("percent-complete-textbox", newPercentComplete);
+ if (gInTab) {
+ sendMessage({
+ command: "updateConfigState",
+ argument: { percentComplete: newPercentComplete }
+ });
+ }
+}
+
+/**
+ * Saves all dialog controls back to the item.
+ *
+ * @return a copy of the original item with changes made.
+ */
+function saveItem() {
+ // we need to clone the item in order to apply the changes.
+ // it is important to not apply the changes to the original item
+ // (even if it happens to be mutable) in order to guarantee
+ // that providers see a proper oldItem/newItem pair in case
+ // they rely on this fact (e.g. WCAP does).
+ let originalItem = window.calendarItem;
+ let item = originalItem.clone();
+
+ // override item's recurrenceInfo *before* serializing date/time-objects.
+ if (!item.recurrenceId) {
+ item.recurrenceInfo = window.recurrenceInfo;
+ }
+
+ // serialize the item
+ saveDialog(item);
+
+ item.organizer = window.organizer;
+
+ item.removeAllAttendees();
+ if (window.attendees && (window.attendees.length > 0)) {
+ for (let attendee of window.attendees) {
+ item.addAttendee(attendee);
+ }
+
+ let notifyCheckbox = document.getElementById("notify-attendees-checkbox");
+ if (notifyCheckbox.disabled) {
+ item.deleteProperty("X-MOZ-SEND-INVITATIONS");
+ } else {
+ item.setProperty("X-MOZ-SEND-INVITATIONS", notifyCheckbox.checked ? "TRUE" : "FALSE");
+ }
+ let undiscloseCheckbox = document.getElementById("undisclose-attendees-checkbox");
+ if (undiscloseCheckbox.disabled) {
+ item.deleteProperty("X-MOZ-SEND-INVITATIONS-UNDISCLOSED");
+ } else {
+ item.setProperty("X-MOZ-SEND-INVITATIONS-UNDISCLOSED",
+ undiscloseCheckbox.checked ? "TRUE" : "FALSE");
+ }
+ let disallowcounterCheckbox = document.getElementById("disallow-counter-checkbox");
+ let xProp = window.calendarItem.getProperty("X-MICROSOFT-DISALLOW-COUNTER");
+ // we want to leave an existing x-prop in case the checkbox is disabled as we need to
+ // roundtrip x-props that are not exclusively under our control
+ if (!disallowcounterCheckbox.disabled) {
+ // we only set the prop if we need to
+ if (disallowcounterCheckbox.checked) {
+ item.setProperty("X-MICROSOFT-DISALLOW-COUNTER", "TRUE");
+ } else if (xProp) {
+ item.setProperty("X-MICROSOFT-DISALLOW-COUNTER", "FALSE");
+ }
+ }
+ }
+
+ // We check if the organizerID is different from our
+ // calendar-user-address-set. The organzerID is the owner of the calendar.
+ // If it's different, that is because someone is acting on behalf of
+ // the organizer.
+ if (item.organizer && item.calendar.aclEntry) {
+ let userAddresses = item.calendar.aclEntry.getUserAddresses({});
+ if (userAddresses.length > 0 &&
+ !cal.attendeeMatchesAddresses(item.organizer, userAddresses)) {
+ let organizer = item.organizer.clone();
+ organizer.setProperty("SENT-BY", "mailto:" + userAddresses[0]);
+ item.organizer = organizer;
+ }
+ }
+ return item;
+}
+
+/**
+ * Action to take when the user chooses to save. This can happen either by
+ * saving directly or the user selecting to save after being prompted when
+ * closing the dialog.
+ *
+ * This function also takes care of notifying this dialog's caller that the item
+ * is saved.
+ *
+ * @param aIsClosing If true, the save action originates from the
+ * save prompt just before the window is closing.
+ */
+function onCommandSave(aIsClosing) {
+ // The datepickers need to remove the focus in order to trigger the
+ // validation of the values just edited, with the keyboard, but not yet
+ // confirmed (i.e. not followed by a click, a tab or enter keys pressure).
+ document.documentElement.focus();
+
+ // Don't save if a warning dialog about a wrong input date must be showed.
+ if (gWarning) {
+ return;
+ }
+
+ eventDialogCalendarObserver.cancel();
+
+ let originalItem = window.calendarItem;
+ let item = saveItem();
+ let calendar = getCurrentCalendar();
+
+ item.makeImmutable();
+ // Set the item for now, the callback below will set the full item when the
+ // call succeeded
+ window.calendarItem = item;
+
+ // When the call is complete, we need to set the new item, so that the
+ // dialog is up to date.
+
+ // XXX Do we want to disable the dialog or at least the save button until
+ // the call is complete? This might help when the user tries to save twice
+ // before the call is complete. In that case, we do need a progress bar and
+ // the ability to cancel the operation though.
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onOperationComplete: function(aCalendar, aStatus, aOpType, aId, aItem) {
+ // Check if the current window has a calendarItem first, because in case of undo
+ // window refers to the main window and we would get a 'calendarItem is undefined' warning.
+ if (!aIsClosing && "calendarItem" in window) {
+ // If we changed the calendar of the item, onOperationComplete will be called multiple
+ // times. We need to make sure we're receiving the update on the right calendar.
+ if ((!window.calendarItem.id ||aId == window.calendarItem.id) &&
+ (aCalendar.id == window.calendarItem.calendar.id) &&
+ Components.isSuccessCode(aStatus)) {
+ if (window.calendarItem.recurrenceId) {
+ // TODO This workaround needs to be removed in bug 396182
+ // We are editing an occurrence. Make sure that the returned
+ // item is the same occurrence, not its parent item.
+ let occ = aItem.recurrenceInfo
+ .getOccurrenceFor(window.calendarItem.recurrenceId);
+ window.calendarItem = occ;
+ } else {
+ // We are editing the parent item, no workarounds needed
+ window.calendarItem = aItem;
+ }
+
+ // We now have an item, so we must change to an edit.
+ window.mode = "modify";
+ updateTitle();
+ eventDialogCalendarObserver.observe(window.calendarItem.calendar);
+ }
+ }
+ // this triggers the update of the imipbar in case this is a rescheduling case
+ if (window.counterProposal && window.counterProposal.onReschedule) {
+ window.counterProposal.onReschedule();
+ }
+ },
+ onGetResult: function() {}
+ };
+ window.onAcceptCallback(item, calendar, originalItem, listener);
+}
+
+/**
+ * This function is called when the user chooses to delete an Item
+ * from the Event/Task dialog
+ *
+ */
+function onCommandDeleteItem() {
+ // only ask for confirmation, if the User changed anything on a new item or we modify an existing item
+ if (isItemChanged() || window.mode != "new") {
+ let promptTitle = "";
+ let promptMessage = "";
+
+ if (cal.isEvent(window.calendarItem)) {
+ promptTitle = calGetString("calendar", "deleteEventLabel");
+ promptMessage = calGetString("calendar", "deleteEventMessage");
+ } else if (cal.isToDo(window.calendarItem)) {
+ promptTitle = calGetString("calendar", "deleteTaskLabel");
+ promptMessage = calGetString("calendar", "deleteTaskMessage");
+ }
+
+ let answerDelete = Services.prompt.confirm(
+ null,
+ promptTitle,
+ promptMessage);
+ if (!answerDelete) {
+ return;
+ }
+ }
+
+ if (window.mode == "new") {
+ cancelItem();
+ } else {
+ let deleteListener = {
+ // when deletion of item is complete, close the dialog
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ // Check if the current window has a calendarItem first, because in case of undo
+ // window refers to the main window and we would get a 'calendarItem is undefined' warning.
+ if ("calendarItem" in window) {
+ if (aId == window.calendarItem.id && Components.isSuccessCode(aStatus)) {
+ cancelItem();
+ } else {
+ eventDialogCalendarObserver.observe(window.calendarItem.calendar);
+ }
+ }
+ }
+ };
+
+ eventDialogCalendarObserver.cancel();
+ if (window.calendarItem.parentItem.recurrenceInfo && window.calendarItem.recurrenceId) {
+ // if this is a single occurrence of a recurring item
+ let newItem = window.calendarItem.parentItem.clone();
+ newItem.recurrenceInfo.removeOccurrenceAt(window.calendarItem.recurrenceId);
+
+ gMainWindow.doTransaction("modify", newItem, newItem.calendar,
+ window.calendarItem.parentItem, deleteListener);
+ } else {
+ gMainWindow.doTransaction("delete", window.calendarItem, window.calendarItem.calendar,
+ null, deleteListener);
+ }
+ }
+}
+
+/**
+ * Postpone the task's start date/time and due date/time. ISO 8601
+ * format: "PT1H", "P1D", and "P1W" are 1 hour, 1 day, and 1 week. (We
+ * use this format intentionally instead of a calIDuration object because
+ * those objects cannot be serialized for message passing with iframes.)
+ *
+ * @param {string} aDuration A duration in ISO 8601 format
+ */
+function postponeTask(aDuration) {
+ let duration = cal.createDuration(aDuration);
+ if (gStartTime != null) {
+ gStartTime.addDuration(duration);
+ }
+ if (gEndTime != null) {
+ gEndTime.addDuration(duration);
+ }
+ updateDateTime();
+}
+
+/**
+ * Prompts the user to change the start timezone.
+ */
+function editStartTimezone() {
+ editTimezone("timezone-starttime",
+ gStartTime.getInTimezone(gStartTimezone),
+ editStartTimezone.complete);
+}
+editStartTimezone.complete = function(datetime) {
+ let equalTimezones = false;
+ if (gStartTimezone && gEndTimezone) {
+ if (gStartTimezone == gEndTimezone) {
+ equalTimezones = true;
+ }
+ }
+ gStartTimezone = datetime.timezone;
+ if (equalTimezones) {
+ gEndTimezone = datetime.timezone;
+ }
+ updateDateTime();
+};
+
+/**
+ * Prompts the user to change the end timezone.
+ */
+function editEndTimezone() {
+ editTimezone("timezone-endtime",
+ gEndTime.getInTimezone(gEndTimezone),
+ editEndTimezone.complete);
+}
+editEndTimezone.complete = function(datetime) {
+ gEndTimezone = datetime.timezone;
+ updateDateTime();
+};
+
+/**
+ * Called to choose a recent timezone from the timezone popup.
+ *
+ * @param event The event with a target that holds the timezone id value.
+ */
+function chooseRecentTimezone(event) {
+ let tzid = event.target.value;
+ let timezonePopup = document.getElementById("timezone-popup");
+ let tzProvider = getCurrentCalendar().getProperty("timezones.provider") ||
+ cal.getTimezoneService();
+
+ if (tzid != "custom") {
+ let zone = tzProvider.getTimezone(tzid);
+ let datetime = timezonePopup.dateTime.getInTimezone(zone);
+ timezonePopup.editTimezone.complete(datetime);
+ }
+}
+
+/**
+ * Opens the timezone popup on the node the event target points at.
+ *
+ * @param event The event causing the popup to open
+ * @param dateTime The datetime for which the timezone should be modified
+ * @param editFunc The function to be called when the custom menuitem is clicked.
+ */
+function showTimezonePopup(event, dateTime, editFunc) {
+ // Don't do anything for right/middle-clicks. Also, don't show the popup if
+ // the opening node is disabled.
+ if (event.button != 0 || event.target.disabled) {
+ return;
+ }
+
+ let timezonePopup = document.getElementById("timezone-popup");
+ let timezoneDefaultItem = document.getElementById("timezone-popup-defaulttz");
+ let timezoneSeparator = document.getElementById("timezone-popup-menuseparator");
+ let defaultTimezone = cal.calendarDefaultTimezone();
+ let recentTimezones = cal.getRecentTimezones(true);
+
+ // Set up the right editTimezone function, so the custom item can use it.
+ timezonePopup.editTimezone = editFunc;
+ timezonePopup.dateTime = dateTime;
+
+ // Set up the default timezone item
+ timezoneDefaultItem.value = defaultTimezone.tzid;
+ timezoneDefaultItem.label = defaultTimezone.displayName;
+
+ // Clear out any old recent timezones
+ while (timezoneDefaultItem.nextSibling != timezoneSeparator) {
+ timezoneDefaultItem.nextSibling.remove();
+ }
+
+ // Fill in the new recent timezones
+ for (let timezone of recentTimezones) {
+ let menuItem = createXULElement("menuitem");
+ menuItem.setAttribute("value", timezone.tzid);
+ menuItem.setAttribute("label", timezone.displayName);
+ timezonePopup.insertBefore(menuItem, timezoneDefaultItem.nextSibling);
+ }
+
+ // Show the popup
+ timezonePopup.openPopup(event.target, "after_start", 0, 0, true);
+}
+
+/**
+ * Common function of edit(Start|End)Timezone() to prompt the user for a
+ * timezone change.
+ *
+ * @param aElementId The XUL element id of the timezone label.
+ * @param aDateTime The Date/Time of the time to change zone on.
+ * @param aCallback What to do when the user has chosen a zone.
+ */
+function editTimezone(aElementId, aDateTime, aCallback) {
+ if (document.getElementById(aElementId)
+ .hasAttribute("disabled")) {
+ return;
+ }
+
+ // prepare the arguments that will be passed to the dialog
+ let args = {};
+ args.time = aDateTime;
+ args.calendar = getCurrentCalendar();
+ args.onOk = function(datetime) {
+ cal.saveRecentTimezone(datetime.timezone.tzid);
+ return aCallback(datetime);
+ };
+
+ // open the dialog modally
+ openDialog(
+ "chrome://calendar/content/calendar-event-dialog-timezone.xul",
+ "_blank",
+ "chrome,titlebar,modal,resizable",
+ args);
+}
+
+/**
+ * This function initializes the following controls:
+ * - 'event-starttime'
+ * - 'event-endtime'
+ * - 'event-all-day'
+ * - 'todo-has-entrydate'
+ * - 'todo-entrydate'
+ * - 'todo-has-duedate'
+ * - 'todo-duedate'
+ * The date/time-objects are either displayed in their respective
+ * timezone or in the default timezone. This decision is based
+ * on whether or not 'cmd_timezone' is checked.
+ * the necessary information is taken from the following variables:
+ * - 'gStartTime'
+ * - 'gEndTime'
+ * - 'window.calendarItem' (used to decide about event/task)
+ */
+function updateDateTime() {
+ gIgnoreUpdate = true;
+
+ let item = window.calendarItem;
+ // Convert to default timezone if the timezone option
+ // is *not* checked, otherwise keep the specific timezone
+ // and display the labels in order to modify the timezone.
+ if (gTimezonesEnabled) {
+ if (isEvent(item)) {
+ let startTime = gStartTime.getInTimezone(gStartTimezone);
+ let endTime = gEndTime.getInTimezone(gEndTimezone);
+
+ setElementValue("event-all-day", startTime.isDate, "checked");
+
+ // In the case where the timezones are different but
+ // the timezone of the endtime is "UTC", we convert
+ // the endtime into the timezone of the starttime.
+ if (startTime && endTime) {
+ if (!compareObjects(startTime.timezone, endTime.timezone)) {
+ if (endTime.timezone.isUTC) {
+ endTime = endTime.getInTimezone(startTime.timezone);
+ }
+ }
+ }
+
+ // before feeding the date/time value into the control we need
+ // to set the timezone to 'floating' in order to avoid the
+ // automatic conversion back into the OS timezone.
+ startTime.timezone = floating();
+ endTime.timezone = floating();
+
+ setElementValue("event-starttime", cal.dateTimeToJsDate(startTime));
+ setElementValue("event-endtime", cal.dateTimeToJsDate(endTime));
+ }
+
+ if (isToDo(item)) {
+ let startTime = gStartTime && gStartTime.getInTimezone(gStartTimezone);
+ let endTime = gEndTime && gEndTime.getInTimezone(gEndTimezone);
+ let hasEntryDate = (startTime != null);
+ let hasDueDate = (endTime != null);
+
+ if (hasEntryDate && hasDueDate) {
+ setElementValue("todo-has-entrydate", hasEntryDate, "checked");
+ startTime.timezone = floating();
+ setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime));
+
+ setElementValue("todo-has-duedate", hasDueDate, "checked");
+ endTime.timezone = floating();
+ setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime));
+ } else if (hasEntryDate) {
+ setElementValue("todo-has-entrydate", hasEntryDate, "checked");
+ startTime.timezone = floating();
+ setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime));
+
+ startTime.timezone = floating();
+ setElementValue("todo-duedate", cal.dateTimeToJsDate(startTime));
+ } else if (hasDueDate) {
+ endTime.timezone = floating();
+ setElementValue("todo-entrydate", cal.dateTimeToJsDate(endTime));
+
+ setElementValue("todo-has-duedate", hasDueDate, "checked");
+ endTime.timezone = floating();
+ setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime));
+ } else {
+ startTime = window.initialStartDateValue;
+ startTime.timezone = floating();
+ endTime = startTime.clone();
+
+ setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime));
+ setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime));
+ }
+ }
+ } else {
+ let kDefaultTimezone = calendarDefaultTimezone();
+
+ if (isEvent(item)) {
+ let startTime = gStartTime.getInTimezone(kDefaultTimezone);
+ let endTime = gEndTime.getInTimezone(kDefaultTimezone);
+ setElementValue("event-all-day", startTime.isDate, "checked");
+
+ // before feeding the date/time value into the control we need
+ // to set the timezone to 'floating' in order to avoid the
+ // automatic conversion back into the OS timezone.
+ startTime.timezone = floating();
+ endTime.timezone = floating();
+ setElementValue("event-starttime", cal.dateTimeToJsDate(startTime));
+ setElementValue("event-endtime", cal.dateTimeToJsDate(endTime));
+ }
+
+ if (isToDo(item)) {
+ let startTime = gStartTime &&
+ gStartTime.getInTimezone(kDefaultTimezone);
+ let endTime = gEndTime && gEndTime.getInTimezone(kDefaultTimezone);
+ let hasEntryDate = (startTime != null);
+ let hasDueDate = (endTime != null);
+
+ if (hasEntryDate && hasDueDate) {
+ setElementValue("todo-has-entrydate", hasEntryDate, "checked");
+ startTime.timezone = floating();
+ setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime));
+
+ setElementValue("todo-has-duedate", hasDueDate, "checked");
+ endTime.timezone = floating();
+ setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime));
+ } else if (hasEntryDate) {
+ setElementValue("todo-has-entrydate", hasEntryDate, "checked");
+ startTime.timezone = floating();
+ setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime));
+
+ startTime.timezone = floating();
+ setElementValue("todo-duedate", cal.dateTimeToJsDate(startTime));
+ } else if (hasDueDate) {
+ endTime.timezone = floating();
+ setElementValue("todo-entrydate", cal.dateTimeToJsDate(endTime));
+
+ setElementValue("todo-has-duedate", hasDueDate, "checked");
+ endTime.timezone = floating();
+ setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime));
+ } else {
+ startTime = window.initialStartDateValue;
+ startTime.timezone = floating();
+ endTime = startTime.clone();
+
+ setElementValue("todo-entrydate", cal.dateTimeToJsDate(startTime));
+ setElementValue("todo-duedate", cal.dateTimeToJsDate(endTime));
+ }
+ }
+ }
+
+ updateTimezone();
+ updateAllDay();
+ updateRepeatDetails();
+
+ gIgnoreUpdate = false;
+}
+
+/**
+ * This function initializes the following controls:
+ * - 'timezone-starttime'
+ * - 'timezone-endtime'
+ * the timezone-links show the corrosponding names of the
+ * start/end times. If 'cmd_timezone' is not checked
+ * the links will be collapsed.
+ */
+function updateTimezone() {
+ function updateTimezoneElement(aTimezone, aId, aDateTime) {
+ let element = document.getElementById(aId);
+ if (!element) {
+ return;
+ }
+
+ if (aTimezone) {
+ element.removeAttribute("collapsed");
+ element.value = aTimezone.displayName || aTimezone.tzid;
+ if (!aDateTime || !aDateTime.isValid || gIsReadOnly || aDateTime.isDate) {
+ if (element.hasAttribute("class")) {
+ element.setAttribute("class-on-enabled",
+ element.getAttribute("class"));
+ element.removeAttribute("class");
+ }
+ if (element.hasAttribute("onclick")) {
+ element.setAttribute("onclick-on-enabled",
+ element.getAttribute("onclick"));
+ element.removeAttribute("onclick");
+ }
+ element.setAttribute("disabled", "true");
+ } else {
+ if (element.hasAttribute("class-on-enabled")) {
+ element.setAttribute("class",
+ element.getAttribute("class-on-enabled"));
+ element.removeAttribute("class-on-enabled");
+ }
+ if (element.hasAttribute("onclick-on-enabled")) {
+ element.setAttribute("onclick",
+ element.getAttribute("onclick-on-enabled"));
+ element.removeAttribute("onclick-on-enabled");
+ }
+ element.removeAttribute("disabled");
+ }
+ } else {
+ element.setAttribute("collapsed", "true");
+ }
+ }
+
+ // convert to default timezone if the timezone option
+ // is *not* checked, otherwise keep the specific timezone
+ // and display the labels in order to modify the timezone.
+ if (gTimezonesEnabled) {
+ updateTimezoneElement(gStartTimezone,
+ "timezone-starttime",
+ gStartTime);
+ updateTimezoneElement(gEndTimezone,
+ "timezone-endtime",
+ gEndTime);
+ } else {
+ document.getElementById("timezone-starttime")
+ .setAttribute("collapsed", "true");
+ document.getElementById("timezone-endtime")
+ .setAttribute("collapsed", "true");
+ }
+}
+
+/**
+ * Updates dialog controls related to item attachments
+ */
+function updateAttachment() {
+ let hasAttachments = capSupported("attachments");
+ if (!gNewItemUI) {
+ setElementValue("cmd_attach_url", !hasAttachments && "true", "disabled");
+ }
+ sendMessage({
+ command: "updateConfigState",
+ argument: { attachUrlCommand: hasAttachments }
+ });
+}
+
+/**
+ * Returns whether to show or hide the related link on the dialog
+ * (rfc2445 URL property). The aShow argument passed in may be overridden
+ * for various reasons.
+ *
+ * @param {boolean} aShow Show the link (true) or not (false)
+ * @param {string} aUrl The url in question
+ * @return {boolean} Returns true for show and false for hide
+ */
+function showOrHideItemURL(aShow, aUrl) {
+ if (aShow && aUrl.length) {
+ let handler;
+ let uri;
+ try {
+ uri = makeURL(aUrl);
+ handler = Services.io.getProtocolHandler(uri.scheme);
+ } catch (e) {
+ // No protocol handler for the given protocol, or invalid uri
+ // hideOrShow(false);
+ return false;
+ }
+ // Only show if its either an internal protcol handler, or its external
+ // and there is an external app for the scheme
+ handler = cal.wrapInstance(handler, Components.interfaces.nsIExternalProtocolHandler);
+ return !handler || handler.externalAppExistsForScheme(uri.scheme);
+ } else {
+ // Hide if there is no url, or the menuitem was chosen so that the url
+ // should be hidden.
+ return false;
+ }
+}
+
+/**
+ * Updates the related link on the dialog (rfc2445 URL property).
+ *
+ * @param {boolean} aShow Show the link (true) or not (false)
+ * @param {string} aUrl The url
+ */
+function updateItemURL(aShow, aUrl) {
+ // Hide or show the link
+ setElementValue("event-grid-link-row", !aShow && "true", "hidden");
+ // The separator is not there in the summary dialog
+ let separator = document.getElementById("event-grid-link-separator");
+ if (separator) {
+ setElementValue("event-grid-link-separator", !aShow && "true", "hidden");
+ }
+
+ // Set the url for the link
+ if (aShow && aUrl.length) {
+ setTimeout(() => {
+ // HACK the url-link doesn't crop when setting the value in onLoad
+ setElementValue("url-link", aUrl);
+ setElementValue("url-link", aUrl, "href");
+ }, 0);
+ }
+}
+
+
+/**
+ * This function updates dialog controls related to attendees.
+ */
+function updateAttendees() {
+ // sending email invitations currently only supported for events
+ let attendeeTab = document.getElementById("event-grid-tab-attendees");
+ let attendeePanel = document.getElementById("event-grid-tabpanel-attendees");
+ if (isEvent(window.calendarItem)) {
+ attendeeTab.removeAttribute("collapsed");
+ attendeePanel.removeAttribute("collapsed");
+
+ if (window.organizer && window.organizer.id) {
+ document.getElementById("item-organizer-row").removeAttribute("collapsed");
+ let cell = document.querySelector(".item-organizer-cell");
+ let icon = cell.querySelector("img:nth-of-type(1)");
+ let text = cell.querySelector("label:nth-of-type(1)");
+
+ let role = organizer.role || "REQ-PARTICIPANT";
+ let userType = organizer.userType || "INDIVIDUAL";
+ let partStat = organizer.participationStatus || "NEEDS-ACTION";
+
+ let orgName = (organizer.commonName && organizer.commonName.length)
+ ? organizer.commonName : organizer.toString();
+ let userTypeString = cal.calGetString("calendar", "dialog.tooltip.attendeeUserType2." + userType,
+ [organizer.toString()]);
+ let roleString = cal.calGetString("calendar", "dialog.tooltip.attendeeRole2." + role,
+ [userTypeString]);
+ let partStatString = cal.calGetString("calendar", "dialog.tooltip.attendeePartStat2." + partStat,
+ [orgName]);
+ let tooltip = cal.calGetString("calendar", "dialog.tooltip.attendee.combined",
+ [roleString, partStatString]);
+
+ text.setAttribute("value", orgName);
+ cell.setAttribute("tooltiptext", tooltip);
+ icon.setAttribute("partstat", partStat);
+ icon.setAttribute("usertype", userType);
+ icon.setAttribute("role", role);
+ } else {
+ setBooleanAttribute("item-organizer-row", "collapsed", true);
+ }
+ setupAttendees();
+ } else {
+ attendeeTab.setAttribute("collapsed", "true");
+ attendeePanel.setAttribute("collapsed", "true");
+ }
+}
+
+/**
+ * This function updates dialog controls related to recurrence, in this case the
+ * text describing the recurrence rule.
+ */
+function updateRepeatDetails() {
+ // Don't try to show the details text for
+ // anything but a custom recurrence rule.
+ let recurrenceInfo = window.recurrenceInfo;
+ let itemRepeat = document.getElementById("item-repeat");
+ if (itemRepeat.value == "custom" && recurrenceInfo) {
+ let item = window.calendarItem;
+ document.getElementById("repeat-deck").selectedIndex = 1;
+ // First of all collapse the details text. If we fail to
+ // create a details string, we simply don't show anything.
+ // this could happen if the repeat rule is something exotic
+ // we don't have any strings prepared for.
+ let repeatDetails = document.getElementById("repeat-details");
+ repeatDetails.setAttribute("collapsed", "true");
+
+ // Try to create a descriptive string from the rule(s).
+ let kDefaultTimezone = calendarDefaultTimezone();
+ let event = cal.isEvent(item);
+
+ let startDate = getElementValue(event ? "event-starttime" : "todo-entrydate");
+ let endDate = getElementValue(event ? "event-endtime" : "todo-duedate");
+ startDate = cal.jsDateToDateTime(startDate, kDefaultTimezone);
+ endDate = cal.jsDateToDateTime(endDate, kDefaultTimezone);
+
+ let allDay = getElementValue("event-all-day", "checked");
+ let detailsString = recurrenceRule2String(recurrenceInfo, startDate,
+ endDate, allDay);
+
+ if (!detailsString) {
+ detailsString = cal.calGetString("calendar-event-dialog", "ruleTooComplex");
+ }
+
+ // Now display the string...
+ let lines = detailsString.split("\n");
+ repeatDetails.removeAttribute("collapsed");
+ while (repeatDetails.childNodes.length > lines.length) {
+ repeatDetails.lastChild.remove();
+ }
+ let numChilds = repeatDetails.childNodes.length;
+ for (let i = 0; i < lines.length; i++) {
+ if (i >= numChilds) {
+ let newNode = repeatDetails.childNodes[0]
+ .cloneNode(true);
+ repeatDetails.appendChild(newNode);
+ }
+ repeatDetails.childNodes[i].value = lines[i];
+ repeatDetails.childNodes[i].setAttribute("tooltiptext",
+ detailsString);
+ }
+ } else {
+ let repeatDetails = document.getElementById("repeat-details");
+ repeatDetails.setAttribute("collapsed", "true");
+ }
+}
+
+/**
+ * This function does not strictly check if the given attendee has the status
+ * TENTATIVE, but also if he hasn't responded.
+ *
+ * @param aAttendee The attendee to check.
+ * @return True, if the attendee hasn't responded.
+ */
+function isAttendeeUndecided(aAttendee) {
+ return aAttendee.participationStatus != "ACCEPTED" &&
+ aAttendee.participationStatus != "DECLINED" &&
+ aAttendee.participationStatus != "DELEGATED";
+}
+
+/**
+ * Event handler for dblclick on attendee items.
+ *
+ * @param aEvent The popupshowing event
+ */
+function attendeeDblClick(aEvent) {
+ // left mouse button
+ if (aEvent.button == 0) {
+ editAttendees();
+ }
+ return;
+}
+
+/**
+ * Event handler to set up the attendee-popup. This builds the popup menuitems.
+ *
+ * @param aEvent The popupshowing event
+ */
+function attendeeClick(aEvent) {
+ // we need to handle right clicks only to display the context menu
+ if (aEvent.button != 2) {
+ return;
+ }
+
+ if (window.attendees.length == 0) {
+ // we just need the option to open the attendee dialog in this case
+ let popup = document.getElementById("attendee-popup");
+ let invite = document.getElementById("attendee-popup-invite-menuitem");
+ for (let node of popup.childNodes) {
+ if (node == invite) {
+ showElement(node);
+ } else {
+ hideElement(node);
+ }
+ }
+ } else {
+ if (window.attendees.length > 1) {
+ let removeall = document.getElementById("attendee-popup-removeallattendees-menuitem");
+ showElement(removeall);
+ }
+ let sendEmail = document.getElementById("attendee-popup-sendemail-menuitem");
+ let sendTentativeEmail = document.getElementById("attendee-popup-sendtentativeemail-menuitem");
+ let firstSeparator = document.getElementById("attendee-popup-first-separator");
+ [sendEmail, sendTentativeEmail, firstSeparator].forEach(showElement);
+
+ // setup attendee specific menu items if appropriate otherwise hide respective menu items
+ let mailto = document.getElementById("attendee-popup-emailattendee-menuitem");
+ let remove = document.getElementById("attendee-popup-removeattendee-menuitem");
+ let secondSeparator = document.getElementById("attendee-popup-second-separator");
+ let attId = aEvent.target.parentNode.getAttribute("attendeeid");
+ let attendee = window.attendees.find(aAtt => aAtt.id == attId);
+ if (attendee) {
+ [mailto, remove, secondSeparator].forEach(showElement);
+ mailto.setAttribute("label", attendee.toString());
+ mailto.attendee = attendee;
+ remove.attendee = attendee;
+ } else {
+ [mailto, remove, secondSeparator].forEach(hideElement);
+ }
+
+ if (window.attendees.some(isAttendeeUndecided)) {
+ document.getElementById("cmd_email_undecided")
+ .removeAttribute("disabled");
+ } else {
+ document.getElementById("cmd_email_undecided")
+ .setAttribute("disabled", "true");
+ }
+ }
+}
+
+/**
+ * Removes the selected attendee from the window
+ * @param aAttendee
+ */
+function removeAttendee(aAttendee) {
+ if (aAttendee) {
+ window.attendees = window.attendees.filter(aAtt => aAtt != aAttendee);
+ updateAttendees();
+ }
+}
+
+/**
+ * Removes all attendees from the window
+ */
+function removeAllAttendees() {
+ window.attendees = [];
+ window.organizer = null;
+ updateAttendees();
+}
+
+/**
+ * Send Email to all attendees that haven't responded or are tentative.
+ *
+ * @param aAttendees The attendees to check.
+ */
+function sendMailToUndecidedAttendees(aAttendees) {
+ let targetAttendees = attendees.filter(isAttendeeUndecided);
+ sendMailToAttendees(targetAttendees);
+}
+
+/**
+ * Send Email to all given attendees.
+ *
+ * @param aAttendees The attendees to send mail to.
+ */
+function sendMailToAttendees(aAttendees) {
+ let toList = cal.getRecipientList(aAttendees);
+ let item = saveItem();
+ let emailSubject = cal.calGetString("calendar-event-dialog", "emailSubjectReply", [item.title]);
+ let identity = window.calendarItem.calendar.getProperty("imip.identity");
+ sendMailTo(toList, emailSubject, null, identity);
+}
+
+/**
+ * Make sure all fields that may have calendar specific capabilities are updated
+ */
+function updateCapabilities() {
+ updateAttachment();
+ updateConfigState({
+ priority: gConfig.priority,
+ privacy: gConfig.privacy
+ });
+ updateReminderDetails();
+ updateCategoryMenulist();
+}
+
+/**
+ * find out if the User already changed values in the Dialog
+ *
+ * @return: true if the values in the Dialog have changed. False otherwise.
+ */
+function isItemChanged() {
+ let newItem = saveItem();
+ let oldItem = window.calendarItem.clone();
+
+ // we need to guide the description text through the text-field since
+ // newlines are getting converted which would indicate changes to the
+ // text.
+ setElementValue("item-description", oldItem.getProperty("DESCRIPTION"));
+ setItemProperty(oldItem,
+ "DESCRIPTION",
+ getElementValue("item-description"));
+ setElementValue("item-description", newItem.getProperty("DESCRIPTION"));
+
+ if ((newItem.calendar.id == oldItem.calendar.id) &&
+ compareItemContent(newItem, oldItem)) {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Test if a specific capability is supported
+ *
+ * @param aCap The capability from "capabilities.<aCap>.supported"
+ */
+function capSupported(aCap) {
+ let calendar = getCurrentCalendar();
+ return calendar.getProperty("capabilities." + aCap + ".supported") !== false;
+}
+
+/**
+ * Return the values for a certain capability.
+ *
+ * @param aCap The capability from "capabilities.<aCap>.values"
+ * @return The values for this capability
+ */
+function capValues(aCap, aDefault) {
+ let calendar = getCurrentCalendar();
+ let vals = calendar.getProperty("capabilities." + aCap + ".values");
+ return (vals === null ? aDefault : vals);
+}
+
+/**
+ * Checks the until date just entered in the datepicker in order to avoid
+ * setting a date earlier than the start date.
+ * Restores the previous correct date; sets the warning flag to prevent closing
+ * the dialog when the user enters a wrong until date.
+ */
+function checkUntilDate() {
+ let repeatUntilDate = getElementValue("repeat-until-datepicker");
+ if (repeatUntilDate == "forever") {
+ updateRepeat();
+ // "forever" is never earlier than another date.
+ return;
+ }
+
+ // Check whether the date is valid. Set the correct time just in this case.
+ let untilDate = cal.jsDateToDateTime(repeatUntilDate, gStartTime.timezone);
+ let startDate = gStartTime.clone();
+ startDate.isDate = true;
+ if (untilDate.compare(startDate) < 0) {
+ // Invalid date: restore the previous date. Since we are checking an
+ // until date, a null value for gUntilDate means repeat "forever".
+ setElementValue("repeat-until-datepicker",
+ gUntilDate ? cal.dateTimeToJsDate(gUntilDate.getInTimezone(cal.floating()))
+ : "forever");
+ gWarning = true;
+ let callback = function() {
+ // Disable the "Save" and "Save and Close" commands as long as the
+ // warning dialog is showed.
+ enableAcceptCommand(false);
+
+ Services.prompt.alert(
+ null,
+ document.title,
+ cal.calGetString("calendar", "warningUntilDateBeforeStart"));
+ enableAcceptCommand(true);
+ gWarning = false;
+ };
+ setTimeout(callback, 1);
+ } else {
+ // Valid date: set the time equal to start date time.
+ gUntilDate = untilDate;
+ updateUntildateRecRule();
+ }
+}
+
+/**
+ * Displays a counterproposal if any
+ */
+function displayCounterProposal() {
+ if (!window.counterProposal || !window.counterProposal.attendee ||
+ !window.counterProposal.proposal) {
+ return;
+ }
+
+ let propLabels = document.getElementById("counter-proposal-property-labels");
+ let propValues = document.getElementById("counter-proposal-property-values");
+ let idCounter = 0;
+ let comment;
+
+ for (let proposal of window.counterProposal.proposal) {
+ if (proposal.property == "COMMENT") {
+ if (proposal.proposed && !proposal.original) {
+ comment = proposal.proposed;
+ }
+ } else {
+ let label = lookupCounterLabel(proposal);
+ let value = formatCounterValue(proposal);
+ if (label && value) {
+ // setup label node
+ let propLabel = propLabels.firstChild.cloneNode(false);
+ propLabel.id = propLabel.id + "-" + idCounter;
+ propLabel.control = propLabel.control + "-" + idCounter;
+ propLabel.removeAttribute("collapsed");
+ propLabel.value = label;
+ // setup value node
+ let propValue = propValues.firstChild.cloneNode(false);
+ propValue.id = propLabel.control;
+ propValue.removeAttribute("collapsed");
+ propValue.value = value;
+ // append nodes
+ propLabels.appendChild(propLabel);
+ propValues.appendChild(propValue);
+ idCounter++;
+ }
+ }
+ }
+
+ let attendeeId = window.counterProposal.attendee.CN ||
+ cal.removeMailTo(window.counterProposal.attendee.id || "");
+ let partStat = window.counterProposal.attendee.participationStatus;
+ if (partStat == "DECLINED") {
+ partStat = "counterSummaryDeclined";
+ } else if (partStat == "TENTATIVE") {
+ partStat = "counterSummaryTentative";
+ } else if (partStat == "ACCEPTED") {
+ partStat = "counterSummaryAccepted";
+ } else if (partStat == "DELEGATED") {
+ partStat = "counterSummaryDelegated";
+ } else if (partStat == "NEEDS-ACTION") {
+ partStat = "counterSummaryNeedsAction";
+ } else {
+ cal.LOG("Unexpected partstat " + partStat + " detected.");
+ // we simply reset partStat not display the summary text of the counter box
+ // to avoid the window of death
+ partStat = null;
+ }
+
+ if (idCounter > 0) {
+ if (partStat && attendeeId.length) {
+ document.getElementById("counter-proposal-summary").value = cal.calGetString(
+ "calendar-event-dialog",
+ partStat,
+ [attendeeId]
+ );
+ document.getElementById("counter-proposal-summary").removeAttribute("collapsed");
+ }
+ if (comment) {
+ document.getElementById("counter-proposal-comment").value = comment;
+ document.getElementById("counter-proposal-box").removeAttribute("collapsed");
+ }
+ document.getElementById("counter-proposal-box").removeAttribute("collapsed");
+
+ if (window.counterProposal.oldVersion) {
+ // this is a counterproposal to a previous version of the event - we should notify the
+ // user accordingly
+ notifyUser(
+ "counterProposalOnPreviousVersion",
+ cal.calGetString("calendar-event-dialog", "counterOnPreviousVersionNotification"),
+ "warn"
+ );
+ }
+ if (window.calendarItem.getProperty("X-MICROSOFT-DISALLOW-COUNTER") == "TRUE") {
+ // this is a counterproposal although the user disallowed countering when sending the
+ // invitation, so we notify the user accordingly
+ notifyUser(
+ "counterProposalOnCounteringDisallowed",
+ cal.calGetString("calendar-event-dialog", "counterOnCounterDisallowedNotification"),
+ "warn"
+ );
+ }
+ }
+}
+
+/**
+ * Get the property label to display for a counterproposal based on the respective label used in
+ * the dialog
+ *
+ * @param {JSObject} aProperty The property to check for a label
+ * @returns {String|null} The label to display or null if no such label
+ */
+function lookupCounterLabel(aProperty) {
+ let nodeIds = getPropertyMap();
+ let labels = nodeIds.has(aProperty.property) &&
+ document.getElementsByAttribute("control", nodeIds.get(aProperty.property));
+ let labelValue;
+ if (labels && labels.length) {
+ // as label control assignment should be unique, we can just take the first result
+ labelValue = labels[0].value;
+ } else {
+ cal.LOG("Unsupported property " + aProperty.property + " detected when setting up counter " +
+ "box labels.");
+ }
+ return labelValue;
+}
+
+/**
+ * Get the property value to display for a counterproposal as currently supported
+ *
+ * @param {JSObject} aProperty The property to check for a label
+ * @returns {String|null} The value to display or null if the property is not supported
+ */
+function formatCounterValue(aProperty) {
+ const dateProps = ["DTSTART", "DTEND"];
+ const stringProps = ["SUMMARY", "LOCATION"];
+
+ let val;
+ if (dateProps.includes(aProperty.property)) {
+ let localTime = aProperty.proposed.getInTimezone(cal.calendarDefaultTimezone());
+ let formatter = getDateFormatter();
+ val = formatter.formatDateTime(localTime);
+ if (gTimezonesEnabled) {
+ let tzone = localTime.timezone.displayName || localTime.timezone.tzid;
+ val += " " + tzone;
+ }
+ } else if (stringProps.includes(aProperty.property)) {
+ val = aProperty.proposed;
+ } else {
+ cal.LOG("Unsupported property " + aProperty.property +
+ " detected when setting up counter box values.");
+ }
+ return val;
+}
+
+/**
+ * Get a map of porperty names and labels of currently supported properties
+ *
+ * @returns {Map}
+ */
+function getPropertyMap() {
+ let map = new Map();
+ map.set("SUMMARY", "item-title");
+ map.set("LOCATION", "item-location");
+ map.set("DTSTART", "event-starttime");
+ map.set("DTEND", "event-endtime");
+ return map;
+}
+
+/**
+ * Applies the proposal or original data to the respective dialog fields
+ *
+ * @param {String} aType Either 'proposed' or 'original'
+ */
+function applyValues(aType) {
+ if (!window.counterProposal || (aType != "proposed" && aType != "original")) {
+ return;
+ }
+ let originalBtn = document.getElementById("counter-original-btn");
+ if (originalBtn.disabled) {
+ // The button is disbled when opening the dialog/tab, which makes it more obvious to the
+ // user that he/she needs to apply the proposal values prior to saving & sending.
+ // Once that happened, we leave both options to the user without toogling the button states
+ // to avoid needing to listen to manual changes to do that correctly
+ originalBtn.removeAttribute("disabled");
+ }
+ let nodeIds = getPropertyMap();
+ window.counterProposal.proposal.forEach(aProperty => {
+ if (aProperty.property != "COMMENT") {
+ let valueNode = nodeIds.has(aProperty.property) &&
+ document.getElementById(nodeIds.get(aProperty.property));
+ if (valueNode) {
+ if (["DTSTART", "DTEND"].includes(aProperty.property)) {
+ valueNode.value = cal.dateTimeToJsDate(aProperty[aType]);
+ } else {
+ valueNode.value = aProperty[aType];
+ }
+ }
+ }
+ });
+}
diff --git a/calendar/lightning/content/lightning-item-iframe.xul b/calendar/lightning/content/lightning-item-iframe.xul
new file mode 100644
index 000000000..b18fc425a
--- /dev/null
+++ b/calendar/lightning/content/lightning-item-iframe.xul
@@ -0,0 +1,740 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- XXX some of these css files may not be needed here.
+ widget-bindings.css definitely is needed here -->
+<?xml-stylesheet type="text/css" href="chrome://global/skin/global.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-alarms.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar-common/skin/calendar-attendees.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/widgets/calendar-widget-bindings.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/skin/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/calendar-event-dialog.css"?>
+<?xml-stylesheet type="text/css" href="chrome://calendar/content/datetimepickers/datetimepickers.css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/primaryToolbar.css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/messenger.css"?>
+
+
+<!DOCTYPE window [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ <!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd">
+ <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd">
+ <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd">
+ %brandDTD;
+ %globalDTD;
+ %calendarDTD;
+ %eventDialogDTD;
+]>
+
+<!-- Vbox id is changed during excution to allow different treatment.
+ document.loadOverlay() will not work on this one. -->
+<window id="calendar-event-dialog-inner"
+ onload="onLoad();"
+ onunload="onEventDialogUnload();"
+ onresize="rearrangeAttendees();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- Javascript includes -->
+ <script type="application/javascript"
+ src="chrome://lightning/content/lightning-item-iframe.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-dialog-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calApplicationUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://global/content/printUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://calendar/content/calendar-statusbar.js"/>
+
+ <commandset id="">
+ <command id="cmd_recurrence"
+ oncommand="editRepeat();"/>
+ <command id="cmd_attendees"
+ oncommand="editAttendees();"/>
+ <command id="cmd_email"
+ oncommand="sendMailToAttendees(window.attendees);"/>
+ <command id="cmd_email_undecided"
+ oncommand="sendMailToUndecidedAttendees(window.attendees);"/>
+ <command id="cmd_attach_url"
+ disable-on-readonly="true"
+ oncommand="attachURL()"/>
+ <command id="cmd_attach_cloud"
+ disable-on-readonly="true"/>
+ <command id="cmd_openAttachment"
+ oncommand="openAttachment()"/>
+ <command id="cmd_copyAttachment"
+ oncommand="copyAttachment()"/>
+ <command id="cmd_deleteAttachment"
+ disable-on-readonly="true"
+ oncommand="deleteAttachment()"/>
+ <command id="cmd_deleteAllAttachments"
+ disable-on-readonly="true"
+ oncommand="deleteAllAttachments()"/>
+ <command id="cmd_applyProposal"
+ disable-on-readonly="true"
+ oncommand="applyValues('proposed')"/>
+ <command id="cmd_applyOriginal"
+ disable-on-readonly="true"
+ oncommand="applyValues('original')"/>
+ </commandset>
+
+ <!-- Counter information section -->
+ <hbox id="counter-proposal-box"
+ collapsed="true">
+ <vbox>
+ <description id="counter-proposal-summary"
+ collapsed="true"
+ crop="end" />
+ <hbox id="counter-proposal">
+ <vbox id="counter-proposal-property-labels">
+ <label id="counter-proposal-property-label"
+ control="counter-proposal-property-value"
+ collapsed="true"
+ value="" />
+ </vbox>
+ <vbox id="counter-proposal-property-values">
+ <description id="counter-proposal-property-value"
+ crop="end"
+ collapsed="true"
+ value="" />
+ </vbox>
+ </hbox>
+ <description id="counter-proposal-comment"
+ collapsed="true"
+ crop="end" />
+ </vbox>
+ <spacer flex="1" />
+ <vbox id ="counter-buttons">
+ <button id="counter-proposal-btn"
+ label="&counter.button.proposal.label;"
+ crop="end"
+ command="cmd_applyProposal"
+ orient="horizontal"
+ class="counter-buttons"
+ accesskey="&counter.button.proposal.accesskey;"
+ tooltip="&counter.button.proposal.tooltip2;" />
+ <button id="counter-original-btn"
+ label="&counter.button.original.label;"
+ crop="end"
+ command="cmd_applyOriginal"
+ orient="horizontal"
+ disabled="true"
+ class="counter-buttons"
+ accesskey="&counter.button.original.accesskey;"
+ tooltip="&counter.button.original.tooltip2;" />
+ </vbox>
+ </hbox>
+
+ <notificationbox id="event-dialog-notifications" notificationside="top"/>
+
+ <grid id="event-grid"
+ flex="1"
+ style="padding-top: 8px; padding-bottom: 10px; padding-inline-start: 8px; padding-inline-end: 10px;">
+ <columns id="event-grid-columns">
+ <column id="event-description-column"/>
+ <column id="event-controls-column" flex="1"/>
+ </columns>
+
+ <rows id="event-grid-rows">
+ <!-- Title -->
+ <row id="event-grid-title-row"
+ align="center">
+ <label value="&event.title.textbox.label;"
+ accesskey="&event.title.textbox.accesskey;"
+ control="item-title"
+ disable-on-readonly="true"/>
+ <textbox id="item-title"
+ disable-on-readonly="true"
+ flex="1"
+ oninput="updateTitle()"/>
+ </row>
+
+ <!-- Location -->
+ <row id="event-grid-location-row"
+ align="center">
+ <label value="&event.location.label;"
+ accesskey="&event.location.accesskey;"
+ control="item-location"
+ disable-on-readonly="true"/>
+ <textbox id="item-location"
+ disable-on-readonly="true"/>
+ </row>
+
+ <!-- Category & Calendar -->
+ <row id="event-grid-category-color-row"
+ align="center">
+ <hbox id="event-grid-category-labels-box">
+ <label value="&event.categories.label;"
+ accesskey="&event.categories.accesskey;"
+ control="item-categories"
+ id="item-categories-label"
+ disable-on-readonly="true"/>
+ <label value="&event.calendar.label;"
+ accesskey="&event.calendar.accesskey;"
+ id="item-calendar-aux-label"
+ control="item-calendar"
+ disable-on-readonly="true"/>
+ </hbox>
+ <hbox id="event-grid-category-box" align="center">
+ <menulist id="item-categories"
+ type="panel-menulist"
+ disable-on-readonly="true"
+ flex="1">
+ <panel id="item-categories-panel"
+ type="category-panel"
+ onpopuphiding="updateCategoryMenulist()"/>
+ </menulist>
+ <label value="&event.calendar.label;"
+ accesskey="&event.calendar.accesskey;"
+ control="item-calendar"
+ id="item-calendar-label"
+ disable-on-readonly="true"/>
+ <menulist id="item-calendar"
+ disable-on-readonly="true"
+ flex="1"
+ oncommand="updateCalendar();"/>
+ </hbox>
+ </row>
+
+ <separator class="groove" id="event-grid-basic-separator"/>
+
+ <!-- All-Day -->
+ <row id="event-grid-allday-row"
+ align="center">
+ <spacer/>
+ <checkbox id="event-all-day"
+ class="event-only"
+ disable-on-readonly="true"
+ label="&event.alldayevent.label;"
+ accesskey="&event.alldayevent.accesskey;"
+ oncommand="onUpdateAllDay();"/>
+ </row>
+
+ <!-- StartDate -->
+ <row id="event-grid-startdate-row">
+ <hbox id="event-grid-startdate-label-box"
+ align="center">
+ <label value="&event.from.label;"
+ accesskey="&event.from.accesskey;"
+ control="event-starttime"
+ class="event-only"
+ disable-on-readonly="true"/>
+ <label value="&task.from.label;"
+ accesskey="&task.from.accesskey;"
+ control="todo-has-entrydate"
+ class="todo-only"
+ disable-on-readonly="true"/>
+ </hbox>
+ <hbox id="event-grid-startdate-picker-box">
+ <datetimepicker id="event-starttime"
+ class="event-only"
+ disable-on-readonly="true"
+ onchange="dateTimeControls2State(true);"/>
+ <checkbox id="todo-has-entrydate"
+ class="todo-only checkbox-no-label"
+ disable-on-readonly="true"
+ oncommand="updateEntryDate();"/>
+ <datetimepicker id="todo-entrydate"
+ class="todo-only"
+ disable-on-readonly="true"
+ onchange="dateTimeControls2State(true);"/>
+ <vbox>
+ <hbox>
+ <image id="link-image-top" class="keepduration-link-image" keep="true"/>
+ </hbox>
+ <spacer flex="1"/>
+ <toolbarbutton id="keepduration-button"
+ accesskey="&event.dialog.keepDurationButton.accesskey;"
+ oncommand="toggleKeepDuration();"
+ persist="keep"
+ keep="false"
+ tooltiptext="&event.dialog.keepDurationButton.tooltip;"/>
+ </vbox>
+ <hbox align="center">
+ <label id="timezone-starttime"
+ class="text-link"
+ collapsed="true"
+ crop="end"
+ disable-on-readonly="true"
+ flex="1"
+ hyperlink="true"
+ onclick="showTimezonePopup(event, gStartTime.getInTimezone(gStartTimezone), editStartTimezone)"/>
+ </hbox>
+ </hbox>
+ </row>
+
+ <!-- EndDate -->
+ <row id="event-grid-enddate-row">
+ <hbox id="event-grid-enddate-label-box"
+ align="center">
+ <label value="&event.to.label;"
+ accesskey="&event.to.accesskey;"
+ control="event-endtime"
+ class="event-only"
+ disable-on-readonly="true"/>
+ <label value="&task.to.label;"
+ accesskey="&task.to.accesskey;"
+ control="todo-has-duedate"
+ class="todo-only"
+ disable-on-readonly="true"/>
+ </hbox>
+ <vbox>
+ <hbox id="event-grid-enddate-picker-box">
+ <datetimepicker id="event-endtime"
+ class="event-only"
+ disable-on-readonly="true"
+ onchange="dateTimeControls2State(false);"/>
+ <checkbox id="todo-has-duedate"
+ class="todo-only checkbox-no-label"
+ disable-on-readonly="true"
+ oncommand="updateDueDate();"/>
+ <datetimepicker id="todo-duedate"
+ class="todo-only"
+ disable-on-readonly="true"
+ onchange="dateTimeControls2State(false);"/>
+ <vbox pack="end">
+ <image id="link-image-bottom" class="keepduration-link-image"/>
+ </vbox>
+ <hbox align="center">
+ <label id="timezone-endtime"
+ class="text-link"
+ collapsed="true"
+ crop="end"
+ disable-on-readonly="true"
+ flex="1"
+ hyperlink="true"
+ onclick="showTimezonePopup(event, gEndTime.getInTimezone(gEndTimezone), editEndTimezone)"/>
+ </hbox>
+ </hbox>
+ </vbox>
+ </row>
+
+ <row id="event-grid-todo-status-row"
+ class="todo-only"
+ align="center">
+ <label id="todo-status-label"
+ value="&task.status.label;"
+ accesskey="&task.status.accesskey;"
+ control="todo-status"
+ disable-on-readonly="true"/>
+ <hbox id="event-grid-todo-status-picker-box"
+ align="center">
+ <menulist id="todo-status"
+ class="todo-only"
+ disable-on-readonly="true"
+ oncommand="updateToDoStatus(this.value);">
+ <menupopup id="todo-status-menupopup">
+ <menuitem id="todo-status-none-menuitem"
+ label="&newevent.todoStatus.none.label;"
+ value="NONE"/>
+ <menuitem id="todo-status-needsaction-menuitem"
+ label="&newevent.status.needsaction.label;"
+ value="NEEDS-ACTION"/>
+ <menuitem id="todo-status-inprogress-menuitem"
+ label="&newevent.status.inprogress.label;"
+ value="IN-PROCESS"/>
+ <menuitem id="todo-status-completed-menuitem"
+ label="&newevent.status.completed.label;"
+ value="COMPLETED"/>
+ <menuitem id="todo-status-canceled-menuitem"
+ label="&newevent.todoStatus.cancelled.label;"
+ value="CANCELLED"/>
+ </menupopup>
+ </menulist>
+ <datepicker id="completed-date-picker"
+ class="todo-only"
+ disable-on-readonly="true"
+ disabled="true"
+ value=""/>
+ <textbox id="percent-complete-textbox"
+ type="number"
+ min="0"
+ max="100"
+ disable-on-readonly="true"
+ size="3"
+ oninput="updateToDoStatus('percent-changed')"
+ onselect="updateToDoStatus('percent-changed')"/>
+ <label id="percent-complete-label"
+ class="todo-only"
+ disable-on-readonly="true"
+ value="&newtodo.percentcomplete.label;"/>
+ </hbox>
+ </row>
+
+ <separator id="event-grid-recurrence-separator" class="groove"/>
+
+ <!-- Recurrence -->
+ <row id="event-grid-recurrence-row"
+ align="center">
+ <label value="&event.repeat.label;"
+ accesskey="&event.repeat.accesskey;"
+ control="item-repeat"
+ disable-on-readonly="true"/>
+ <hbox id="event-grid-recurrence-picker-box"
+ align="center"
+ flex="1">
+ <menulist id="item-repeat"
+ disable-on-readonly="true"
+ oncommand="updateRepeat(null, true)">
+ <menupopup id="item-repeat-menupopup">
+ <menuitem id="repeat-none-menuitem"
+ label="&event.repeat.does.not.repeat.label;"
+ selected="true"
+ value="none"/>
+ <menuitem id="repeat-daily-menuitem"
+ label="&event.repeat.daily.label;"
+ value="daily"/>
+ <menuitem id="repeat-weekly-menuitem"
+ label="&event.repeat.weekly.label;"
+ value="weekly"/>
+ <menuitem id="repeat-weekday-menuitem"
+ label="&event.repeat.every.weekday.label;"
+ value="every.weekday"/>
+ <menuitem id="repeat-biweekly-menuitem"
+ label="&event.repeat.bi.weekly.label;"
+ value="bi.weekly"/>
+ <menuitem id="repeat-monthly-menuitem"
+ label="&event.repeat.monthly.label;"
+ value="monthly"/>
+ <menuitem id="repeat-yearly-menuitem"
+ label="&event.repeat.yearly.label;"
+ value="yearly"/>
+ <menuseparator id="item-repeat-separator"/>
+ <menuitem id="repeat-custom-menuitem"
+ label="&event.repeat.custom.label;"
+ value="custom"/>
+ </menupopup>
+ </menulist>
+ <deck id="repeat-deck" selectedIndex="-1">
+ <hbox id="repeat-untilDate" align="center">
+ <label value="&event.until.label;"
+ accesskey="&event.until.accesskey;"
+ control="repeat-until-datepicker"
+ disable-on-readonly="true"/>
+ <datepicker-forever id="repeat-until-datepicker" flex="1"
+ disable-on-readonly="true"
+ onchange="checkUntilDate();"
+ oncommand="checkUntilDate();"
+ value=""/>
+ </hbox>
+ <vbox id="repeat-details" flex="1">
+ <label class="text-link"
+ crop="right"
+ disable-on-readonly="true"
+ hyperlink="true"
+ flex="1"
+ onclick="updateRepeat()"/>
+ </vbox>
+ </deck>
+ </hbox>
+ </row>
+
+ <separator id="event-grid-alarm-separator"
+ class="groove"/>
+
+ <!-- Reminder (Alarm) -->
+ <row id="event-grid-alarm-row"
+ align="center">
+ <label value="&event.reminder.label;"
+ accesskey="&event.reminder.accesskey;"
+ control="item-alarm"
+ disable-on-readonly="true"/>
+ <hbox id="event-grid-alarm-picker-box"
+ align="center">
+ <menulist id="item-alarm"
+ disable-on-readonly="true"
+ oncommand="updateReminder()">
+ <menupopup id="item-alarm-menupopup">
+ <menuitem id="reminder-none-menuitem"
+ label="&event.reminder.none.label;"
+ selected="true"
+ value="none"/>
+ <menuseparator id="reminder-none-separator"/>
+ <menuitem id="reminder-0minutes-menuitem"
+ label="&event.reminder.0minutes.before.label;"
+ length="0"
+ origin="before"
+ relation="START"
+ unit="minutes"/>
+ <menuitem id="reminder-5minutes-menuitem"
+ label="&event.reminder.5minutes.before.label;"
+ length="5"
+ origin="before"
+ relation="START"
+ unit="minutes"/>
+ <menuitem id="reminder-15minutes-menuitem"
+ label="&event.reminder.15minutes.before.label;"
+ length="15"
+ origin="before"
+ relation="START"
+ unit="minutes"/>
+ <menuitem id="reminder-30minutes-menuitem"
+ label="&event.reminder.30minutes.before.label;"
+ length="30"
+ origin="before"
+ relation="START"
+ unit="minutes"/>
+ <menuseparator id="reminder-minutes-separator"/>
+ <menuitem id="reminder-1hour-menuitem"
+ label="&event.reminder.1hour.before.label;"
+ length="1"
+ origin="before"
+ relation="START"
+ unit="hours"/>
+ <menuitem id="reminder-2hours-menuitem"
+ label="&event.reminder.2hours.before.label;"
+ length="2"
+ origin="before"
+ relation="START"
+ unit="hours"/>
+ <menuitem id="reminder-12hours-menuitem"
+ label="&event.reminder.12hours.before.label;"
+ length="12"
+ origin="before"
+ relation="START"
+ unit="hours"/>
+ <menuseparator id="reminder-hours-separator"/>
+ <menuitem id="reminder-1day-menuitem"
+ label="&event.reminder.1day.before.label;"
+ length="1"
+ origin="before"
+ relation="START"
+ unit="days"/>
+ <menuitem id="reminder-2days-menuitem"
+ label="&event.reminder.2days.before.label;"
+ length="2"
+ origin="before"
+ relation="START"
+ unit="days"/>
+ <menuitem id="reminder-1week-menuitem"
+ label="&event.reminder.1week.before.label;"
+ length="7"
+ origin="before"
+ relation="START"
+ unit="days"/>
+ <menuseparator id="reminder-custom-separator"/>
+ <menuitem id="reminder-custom-menuitem"
+ label="&event.reminder.custom.label;"
+ value="custom"/>
+ </menupopup>
+ </menulist>
+ <hbox id="reminder-details">
+ <hbox id="reminder-icon-box"
+ class="alarm-icons-box"
+ align="center"/>
+ <!-- TODO oncommand? onkeypress? -->
+ <label id="reminder-multiple-alarms-label"
+ hidden="true"
+ value="&event.reminder.multiple.label;"
+ class="text-link"
+ disable-on-readonly="true"
+ flex="1"
+ hyperlink="true"
+ onclick="updateReminder()"/>
+ <label id="reminder-single-alarms-label"
+ hidden="true"
+ class="text-link"
+ disable-on-readonly="true"
+ flex="1"
+ hyperlink="true"
+ onclick="updateReminder()"/>
+ </hbox>
+ </hbox>
+ </row>
+
+ <separator id="event-grid-tabbox-separator"
+ class="groove"/>
+
+ <!-- Multi purpose tab box -->
+ <tabbox id="event-grid-tabbox"
+ selectedIndex="0"
+ flex="1">
+ <tabs id="event-grid-tabs">
+ <tab id="event-grid-tab-description"
+ label="&event.description.label;"
+ accesskey="&event.description.accesskey;"/>
+ <tab id="event-grid-tab-attachments"
+ label="&event.attachments.label;"
+ accesskey="&event.attachments.accesskey;"/>
+ <tab id="event-grid-tab-attendees"
+ label="&event.attendees.label;"
+ accesskey="&event.attendees.accesskey;"
+ collapsed="true"/>
+ </tabs>
+ <tabpanels id="event-grid-tabpanels"
+ flex="1">
+ <tabpanel id="event-grid-tabpanel-description">
+ <textbox id="item-description"
+ disable-on-readonly="true"
+ flex="1"
+ multiline="true"
+ rows="12"/>
+ </tabpanel>
+ <tabpanel id="event-grid-tabpanel-attachements">
+ <vbox flex="1">
+ <listbox id="attachment-link"
+ context="attachment-popup"
+ rows="3"
+ flex="1"
+ disable-on-readonly="true"
+ onkeypress="attachmentLinkKeyPress(event)"
+ onclick="attachmentClick(event);"
+ ondblclick="attachmentDblClick(event);"/>
+ </vbox>
+ </tabpanel>
+ <tabpanel id="event-grid-tabpanel-attendees"
+ collapsed="true">
+ <vbox flex="1">
+ <hbox id="item-organizer-row"
+ collapsed="true"
+ align="top"
+ class="item-attendees-row">
+ <label value="&read.only.organizer.label;"/>
+ <hbox class="item-organizer-cell">
+ <img class="itip-icon"/>
+ <label id="item-organizer"
+ class="item-attendees-cell-label"
+ crop="right"/>
+ </hbox>
+ </hbox>
+ <hbox flex="1">
+ <vbox id="item-attendees-box"
+ dialog-type="event"
+ flex="1"
+ context="attendee-popup"
+ onclick="attendeeClick(event)"
+ disable-on-readonly="true"/>
+ </hbox>
+ <hbox id="notify-options" align="center">
+ <checkbox id="notify-attendees-checkbox"
+ label="&event.attendees.notify.label;"
+ accesskey="&event.attendees.notify.accesskey;"
+ oncommand="changeUndiscloseCheckboxStatus();"
+ pack="start"/>
+ <checkbox id="undisclose-attendees-checkbox"
+ label="&event.attendees.notifyundisclosed.label;"
+ accesskey="&event.attendees.notifyundisclosed.accesskey;"
+ tooltiptext="&event.attendees.notifyundisclosed.tooltip;"
+ pack="start"/>
+ <checkbox id="disallow-counter-checkbox"
+ label="&event.attendees.disallowcounter.label;"
+ accesskey="&event.attendees.disallowcounter.accesskey;"
+ tooltiptext="&event.attendees.disallowcounter.tooltip;"
+ pack="start"/>
+ </hbox>
+ </vbox>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+
+ <separator id="event-grid-link-separator"
+ class="groove"
+ hidden="true"/>
+ <row id="event-grid-link-row"
+ align="center"
+ hidden="true">
+ <label value="&event.url.label;"
+ control="url-link"/>
+ <label id="url-link"
+ onclick="launchBrowser(this.getAttribute('href'), event)"
+ oncommand="launchBrowser(this.getAttribute('href'), event)"
+ class="text-link"
+ crop="end"/>
+ </row>
+ </rows>
+ </grid>
+
+
+ <popupset id="event-dialog-popupset">
+ <menupopup id="attendee-popup">
+ <menuitem id="attendee-popup-invite-menuitem"
+ label="&event.invite.attendees.label;"
+ accesskey="&event.invite.attendees.accesskey;"
+ command="cmd_attendees"
+ disable-on-readonly="true"/>
+ <menuitem id="attendee-popup-removeallattendees-menuitem"
+ label="&event.remove.attendees.label2;"
+ accesskey="&event.remove.attendees.accesskey;"
+ oncommand="removeAllAttendees()"
+ disable-on-readonly="true"
+ crop="end"/>
+ <menuitem id="attendee-popup-removeattendee-menuitem"
+ label="&event.remove.attendee.label;"
+ accesskey="&event.remove.attendee.accesskey;"
+ oncommand="removeAttendee(event.target.attendee)"
+ crop="end"/>
+ <menuseparator id="attendee-popup-first-separator"/>
+ <menuitem id="attendee-popup-sendemail-menuitem"
+ label="&event.email.attendees.label;"
+ accesskey="&event.email.attendees.accesskey;"
+ command="cmd_email"/>
+ <menuitem id="attendee-popup-sendtentativeemail-menuitem"
+ label="&event.email.tentative.attendees.label;"
+ accesskey="&event.email.tentative.attendees.accesskey;"
+ command="cmd_email_undecided"/>
+ <menuseparator id="attendee-popup-second-separator"/>
+ <menuitem id="attendee-popup-emailattendee-menuitem"
+ oncommand="sendMailToAttendees([event.target.attendee])"
+ crop="end"/>
+ </menupopup>
+ <menupopup id="attachment-popup">
+ <menuitem id="attachment-popup-open"
+ label="&event.attachments.popup.open.label;"
+ accesskey="&event.attachments.popup.open.accesskey;"
+ command="cmd_openAttachment"/>
+ <menuitem id="attachment-popup-copy"
+ label="&calendar.copylink.label;"
+ accesskey="&calendar.copylink.accesskey;"
+ command="cmd_copyAttachment"/>
+ <menuitem id="attachment-popup-delete"
+ label="&event.attachments.popup.remove.label;"
+ accesskey="&event.attachments.popup.remove.accesskey;"
+ command="cmd_deleteAttachment"/>
+ <menuitem id="attachment-popup-deleteAll"
+ label="&event.attachments.popup.removeAll.label;"
+ accesskey="&event.attachments.popup.removeAll.accesskey;"
+ command="cmd_deleteAllAttachments"/>
+ <menuseparator/>
+ <menuitem id="attachment-popup-attachPage"
+ label="&event.attachments.popup.attachPage.label;"
+ accesskey="&event.attachments.popup.attachPage.accesskey;"
+ command="cmd_attach_url"/>
+ </menupopup>
+ <menupopup id="timezone-popup"
+ position="after_start"
+ oncommand="chooseRecentTimezone(event)">
+ <menuitem id="timezone-popup-defaulttz"/>
+ <menuseparator id="timezone-popup-menuseparator"/>
+ <menuitem id="timezone-custom-menuitem"
+ label="&event.timezone.custom.label;"
+ value="custom"
+ oncommand="this.parentNode.editTimezone()"/>
+ </menupopup>
+ </popupset>
+
+ <!-- attendee box template -->
+ <vbox id="item-attendees-box-template"
+ hidden="true">
+ <hbox flex="1" class="item-attendees-row" equalsize="always" hidden="true">
+ <box class="item-attendees-cell"
+ hidden="true"
+ flex="1"
+ context="attendee-popup"
+ ondblclick="attendeeDblClick(event)"
+ onclick="attendeeClick(event)">
+ <img class="itip-icon"/>
+ <label class="item-attendees-cell-label"
+ crop="end"
+ flex="1"/>
+ </box>
+ <box hidden="true" flex="1"/>
+ </hbox>
+ </vbox>
+</window>
diff --git a/calendar/lightning/content/lightning-item-panel.js b/calendar/lightning/content/lightning-item-panel.js
new file mode 100644
index 000000000..e98d4318a
--- /dev/null
+++ b/calendar/lightning/content/lightning-item-panel.js
@@ -0,0 +1,1096 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported onLoadLightningItemPanel, onAccept, onCancel, onCommandSave,
+ * onCommandDeleteItem, editAttendees, rotatePrivacy, editPrivacy,
+ * rotatePriority, editPriority, rotateStatus, editStatus,
+ * rotateShowTimeAs, editShowTimeAs, updateShowTimeAs, editToDoStatus,
+ * postponeTask, toggleTimezoneLinks, toggleLink, attachURL,
+ * onCommandViewToolbar, onCommandCustomize, attachFileByAccountKey,
+ */
+
+// XXX Need to determine which of these we really need here.
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/PluralForm.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+try {
+ Components.utils.import("resource:///modules/cloudFileAccounts.js");
+} catch (e) {
+ // This will fail on Seamonkey, but thats ok since the pref for cloudfiles
+ // is false, which means the UI will not be shown
+}
+
+// gTabmail is null if we are in a dialog window and not in a tab.
+var gTabmail = document.getElementById("tabmail") || null;
+
+if (!gTabmail) {
+ // In a dialog window the following menu item functions need to be
+ // defined. In a tab they are defined elsewhere. To prevent errors in
+ // the log they are defined here (before the onLoad function is called).
+ /**
+ * Update menu items that rely on focus.
+ */
+ window.goUpdateGlobalEditMenuItems = () => {
+ goUpdateCommand("cmd_undo");
+ goUpdateCommand("cmd_redo");
+ goUpdateCommand("cmd_cut");
+ goUpdateCommand("cmd_copy");
+ goUpdateCommand("cmd_paste");
+ goUpdateCommand("cmd_selectAll");
+ };
+ /**
+ * Update menu items that rely on the current selection.
+ */
+ window.goUpdateSelectEditMenuItems = () => {
+ goUpdateCommand("cmd_cut");
+ goUpdateCommand("cmd_copy");
+ goUpdateCommand("cmd_delete");
+ goUpdateCommand("cmd_selectAll");
+ };
+ /**
+ * Update menu items that relate to undo/redo.
+ */
+ window.goUpdateUndoEditMenuItems = () => {
+ goUpdateCommand("cmd_undo");
+ goUpdateCommand("cmd_redo");
+ };
+ /**
+ * Update menu items that depend on clipboard contents.
+ */
+ window.goUpdatePasteMenuItems = () => {
+ goUpdateCommand("cmd_paste");
+ };
+}
+
+// Stores the ids of the iframes of currently open event/task tabs, used
+// when window is closed to prompt for saving changes.
+var gItemTabIds = [];
+var gItemTabIdsCopy;
+
+// gConfig is used when switching tabs to restore the state of
+// toolbar, statusbar, and menubar for the current tab.
+var gConfig = {
+ isEvent: null,
+ privacy: null,
+ hasPrivacy: null,
+ calendarType: null,
+ privacyValues: null,
+ priority: null,
+ hasPriority: null,
+ status: null,
+ percentComplete: null,
+ showTimeAs: null,
+ // whether commands are enabled or disabled
+ attendeesCommand: null, // cmd_attendees
+ attachUrlCommand: null, // cmd_attach_url
+ timezonesEnabled: false, // cmd_timezone
+ // XXX Currently there is no toolbar button or menu item for
+ // cmd_toggle_link for event/task tabs
+ toggleLinkCommand: null // cmd_toggle_link
+};
+
+/**
+ * Receive an asynchronous message from the iframe.
+ *
+ * @param {MessageEvent} aEvent Contains the message being received
+ */
+function receiveMessage(aEvent) {
+ if (aEvent.origin !== "chrome://lightning") {
+ return;
+ }
+ switch (aEvent.data.command) {
+ case "initializeItemMenu":
+ initializeItemMenu(aEvent.data.label, aEvent.data.accessKey);
+ break;
+ case "disableLinkCommand": {
+ let linkCommand = document.getElementById("cmd_toggle_link");
+ if (linkCommand) {
+ setElementValue(linkCommand, "true", "disabled");
+ }
+ break;
+ }
+ case "cancelDialog":
+ document.documentElement.cancelDialog();
+ break;
+ case "closeWindowOrTab":
+ closeWindowOrTab(aEvent.data.iframeId);
+ break;
+ case "showCmdStatusNone":
+ document.getElementById("cmd_status_none").removeAttribute("hidden");
+ break;
+ case "updateTitle":
+ updateTitle(aEvent.data.argument);
+ break;
+ case "updateConfigState":
+ updateItemTabState(aEvent.data.argument);
+ Object.assign(gConfig, aEvent.data.argument);
+ break;
+ case "enableAcceptCommand":
+ enableAcceptCommand(aEvent.data.argument);
+ break;
+ case "replyToClosingWindowWithTabs":
+ handleWindowClose(aEvent.data.response);
+ break;
+ case "removeDisableAndCollapseOnReadonly":
+ removeDisableAndCollapseOnReadonly();
+ break;
+ case "setElementAttribute": {
+ let arg = aEvent.data.argument;
+ setElementValue(arg.id, arg.value, arg.attribute);
+ break;
+ }
+ case "loadCloudProviders":
+ loadCloudProviders(aEvent.data.items);
+ break;
+ }
+}
+
+window.addEventListener("message", receiveMessage, false);
+
+/**
+ * Send an asynchronous message to an iframe. Additional properties of
+ * aMessage are generally arguments that will be passed to the function
+ * named in aMessage.command. If aIframeId is omitted, the message will
+ * be sent to the iframe of the current tab.
+ *
+ * @param {Object} aMessage Contains the message being sent
+ * @param {string} aMessage.command The name of a function to call
+ * @param {string} aIframeId (optional) id of an iframe to send the message to
+ */
+function sendMessage(aMessage, aIframeId) {
+ let iframeId = gTabmail ? aIframeId || gTabmail.currentTabInfo.iframe.id
+ : "lightning-item-panel-iframe";
+ let iframe = document.getElementById(iframeId);
+ iframe.contentWindow.postMessage(aMessage, "*");
+}
+
+/**
+ * When the user closes the window, this function handles prompting them
+ * to save any unsaved changes for any open item tabs, before closing the
+ * window, or not if 'cancel' was clicked. Requires sending and receiving
+ * async messages from the iframes of all open item tabs.
+ *
+ * @param {boolean} aResponse The response from the tab's iframe
+ */
+function handleWindowClose(aResponse) {
+ if (!aResponse) {
+ // Cancel was clicked, just leave the window open. We're done.
+ return;
+ } else if (gItemTabIdsCopy.length > 0) {
+ // There are more unsaved changes in tabs to prompt the user about.
+ let nextId = gItemTabIdsCopy.shift();
+ sendMessage({ command: "closingWindowWithTabs", id: nextId }, nextId);
+ } else {
+ // Close the window, there are no more unsaved changes in tabs.
+ window.removeEventListener("close", windowCloseListener, false);
+ window.close();
+ }
+}
+
+/**
+ * Listener function for window close. We prevent the window from
+ * closing, then for each open tab we prompt the user to save any
+ * unsaved changes with handleWindowClose.
+ *
+ * @param {Object} aEvent The window close event
+ */
+function windowCloseListener(aEvent) {
+ aEvent.preventDefault();
+ gItemTabIdsCopy = gItemTabIds.slice();
+ handleWindowClose(true);
+}
+
+/**
+ * Load handler for the outer parent context that contains the iframe.
+ *
+ * @param {string} aIframeId (optional) Id of the iframe in this tab
+ * @param {string} aUrl (optional) The url to load in the iframe
+ */
+function onLoadLightningItemPanel(aIframeId, aUrl) {
+ let iframe;
+ let iframeSrc;
+
+ if (!gTabmail) {
+ gTabmail = document.getElementById("tabmail") || null;
+ }
+ if (gTabmail) {
+ // tab case
+ let iframeId = aIframeId || gTabmail.currentTabInfo.iframe.id;
+ iframe = document.getElementById(iframeId);
+ iframeSrc = aUrl;
+
+ // Add a listener to detect close events, prompt user about saving changes.
+ window.addEventListener("close", windowCloseListener, false);
+ } else {
+ // window dialog case
+ iframe = document.createElement("iframe");
+ iframeSrc = window.arguments[0].useNewItemUI
+ ? "chrome://lightning/content/html-item-editing/lightning-item-iframe.html"
+ : "chrome://lightning/content/lightning-item-iframe.xul";
+
+ iframe.setAttribute("id", "lightning-item-panel-iframe");
+ iframe.setAttribute("flex", "1");
+
+ let dialog = document.getElementById("calendar-event-dialog");
+ let statusbar = document.getElementById("status-bar");
+
+ // Note: iframe.contentWindow is undefined before the iframe is inserted here.
+ dialog.insertBefore(iframe, statusbar);
+
+ // Move the args so they are positioned relative to the iframe,
+ // for the window dialog just as they are for the tab.
+ // XXX Should we delete the arguments here in the parent context
+ // so they are only accessible in one place?
+ iframe.contentWindow.arguments = [window.arguments[0]];
+
+ // hide the ok and cancel dialog buttons
+ let accept = document.documentElement.getButton("accept");
+ let cancel = document.documentElement.getButton("cancel");
+ accept.setAttribute("collapsed", "true");
+ cancel.setAttribute("collapsed", "true");
+ cancel.parentNode.setAttribute("collapsed", "true");
+
+ // set toolbar icon color for light or dark themes
+ if (typeof ToolbarIconColor !== "undefined") {
+ ToolbarIconColor.init();
+ }
+ }
+
+ // event or task
+ let calendarItem = iframe.contentWindow.arguments[0].calendarEvent;
+ gConfig.isEvent = cal.isEvent(calendarItem);
+
+ // for tasks in a window dialog, set the dialog id for CSS selection, etc.
+ if (!gTabmail && !gConfig.isEvent) {
+ setDialogId(document.documentElement, "calendar-task-dialog");
+ }
+
+ // timezones enabled
+ gConfig.timezonesEnabled = getTimezoneCommandState();
+ iframe.contentWindow.gTimezonesEnabled = gConfig.timezonesEnabled;
+
+ // toggle link
+ let cmdToggleLink = document.getElementById("cmd_toggle_link");
+ gConfig.toggleLinkCommand = cmdToggleLink.getAttribute("checked") == "true";
+ iframe.contentWindow.gShowLink = gConfig.toggleLinkCommand;
+
+ // set the iframe src, which loads the iframe's contents
+ iframe.setAttribute("src", iframeSrc);
+}
+
+/**
+ * Unload handler for the outer parent context that contains the iframe.
+ * Currently only called for windows and not tabs.
+ */
+function onUnloadLightningItemPanel() {
+ if (!gTabmail) {
+ // window dialog case
+ if (typeof ToolbarIconColor !== "undefined") {
+ ToolbarIconColor.uninit();
+ }
+ }
+}
+
+/**
+ * Updates the UI. Called when a user makes a change and when an
+ * event/task tab is shown. When a tab is shown aArg contains the gConfig
+ * data for that event/task. We pass the full tab state object to the
+ * update functions and they just use the properties they need from it.
+ *
+ * @param {Object} aArg Its properties hold data about the event/task
+ */
+function updateItemTabState(aArg) {
+ const lookup = {
+ privacy: updatePrivacy,
+ priority: updatePriority,
+ status: updateStatus,
+ showTimeAs: updateShowTimeAs,
+ percentComplete: updateMarkCompletedMenuItem,
+ attendeesCommand: updateAttendeesCommand,
+ attachUrlCommand: updateAttachment,
+ timezonesEnabled: updateTimezoneCommand
+ };
+ for (let key of Object.keys(aArg)) {
+ let procedure = lookup[key];
+ if (procedure) {
+ procedure(aArg);
+ }
+ }
+}
+
+/**
+ * When in a window, set Item-Menu label to Event or Task.
+ *
+ * @param {string} aLabel The new name for the menu
+ * @param {string} aAccessKey The access key for the menu
+ */
+function initializeItemMenu(aLabel, aAccessKey) {
+ let menuItem = document.getElementById("item-menu");
+ menuItem.setAttribute("label", aLabel);
+ menuItem.setAttribute("accesskey", aAccessKey);
+}
+
+/**
+ * Handler for when dialog is accepted.
+ */
+function onAccept() {
+ sendMessage({ command: "onAccept" });
+ return false;
+}
+
+/**
+ * Handler for when dialog is canceled.
+ *
+ * @param {string} aIframeId The id of the iframe
+ */
+function onCancel(aIframeId) {
+ sendMessage({ command: "onCancel", iframeId: aIframeId }, aIframeId);
+ // We return false to prevent closing of a window until we
+ // can ask the user about saving any unsaved changes.
+ return false;
+}
+
+/**
+ * Closes tab or window. Called after prompting to save any unsaved changes.
+ *
+ * @param {string} aIframeId The id of the iframe
+ */
+function closeWindowOrTab(iframeId) {
+ if (gTabmail) {
+ if (iframeId) {
+ // Find the tab associated with this iframeId, and close it.
+ let myTabInfo = gTabmail.tabInfo.filter((x) => "iframe" in x && x.iframe.id == iframeId)[0];
+ myTabInfo.allowTabClose = true;
+ gTabmail.closeTab(myTabInfo);
+ } else {
+ gTabmail.currentTabInfo.allowTabClose = true;
+ gTabmail.removeCurrentTab();
+ }
+ } else {
+ window.close();
+ }
+}
+
+/**
+ * Handler for saving the event or task.
+ *
+ * @param {boolean} aIsClosing Is the tab or window closing
+ */
+function onCommandSave(aIsClosing) {
+ sendMessage({ command: "onCommandSave", isClosing: aIsClosing });
+}
+
+/**
+ * Handler for deleting the event or task.
+ */
+function onCommandDeleteItem() {
+ sendMessage({ command: "onCommandDeleteItem" });
+}
+
+/**
+ * Update the title of the tab or window.
+ *
+ * @param {string} aNewTitle The new title
+ */
+function updateTitle(aNewTitle) {
+ if (gTabmail) {
+ gTabmail.currentTabInfo.title = aNewTitle;
+ gTabmail.setTabTitle(gTabmail.currentTabInfo);
+ } else {
+ document.title = aNewTitle;
+ }
+}
+
+/**
+ * Open a new event.
+ */
+function openNewEvent() {
+ sendMessage({ command: "openNewEvent" });
+}
+
+/**
+ * Open a new task.
+ */
+function openNewTask() {
+ sendMessage({ command: "openNewTask" });
+}
+
+
+/**
+ * Open a new Thunderbird compose window.
+ */
+function openNewMessage() {
+ MailServices.compose.OpenComposeWindow(
+ null,
+ null,
+ null,
+ Components.interfaces.nsIMsgCompType.New,
+ Components.interfaces.nsIMsgCompFormat.Default,
+ null,
+ null
+ );
+}
+
+/**
+ * Open a new addressbook window
+ */
+function openNewCardDialog() {
+ window.openDialog(
+ "chrome://messenger/content/addressbook/abNewCardDialog.xul",
+ "",
+ "chrome,modal,resizable=no,centerscreen"
+ );
+}
+
+/**
+ * Handler for edit attendees command.
+ */
+function editAttendees() {
+ sendMessage({ command: "editAttendees" });
+}
+
+/**
+ * Sends a message to set the gConfig values in the iframe.
+ *
+ * @param {Object} aArg Container
+ * @param {string} aArg.privacy (optional) New privacy value
+ * @param {short} aArg.priority (optional) New priority value
+ * @param {string} aArg.status (optional) New status value
+ * @param {string} aArg.showTimeAs (optional) New showTimeAs / transparency value
+ */
+function editConfigState(aArg) {
+ sendMessage({ command: "editConfigState", argument: aArg });
+}
+
+/**
+ * Rotates the Privacy of an item to the next value
+ * following the sequence -> PUBLIC -> CONFIDENTIAL -> PRIVATE ->.
+ */
+function rotatePrivacy() {
+ const states = ["PUBLIC", "CONFIDENTIAL", "PRIVATE"];
+ let oldPrivacy = gConfig.privacy;
+ let newPrivacy = states[(states.indexOf(oldPrivacy) + 1) % states.length];
+ editConfigState({ privacy: newPrivacy });
+}
+
+/**
+ * Handler for changing privacy. aEvent is used for the popup menu
+ * event-privacy-menupopup in the Privacy toolbar button.
+ *
+ * @param {nsIDOMNode} aTarget Has the new privacy in its "value" attribute
+ * @param {XULCommandEvent} aEvent (optional) the UI element selection event
+ */
+function editPrivacy(aTarget, aEvent) {
+ if (aEvent) {
+ aEvent.stopPropagation();
+ }
+ // "privacy" is indeed the correct attribute to use here
+ let newPrivacy = aTarget.getAttribute("privacy");
+ editConfigState({ privacy: newPrivacy });
+}
+
+/**
+ * Updates the UI according to the privacy setting and the selected
+ * calendar. If the selected calendar does not support privacy or only
+ * certain values, these are removed from the UI. This function should
+ * be called any time that privacy setting is updated.
+ *
+ * @param {Object} aArg Contains privacy properties
+ * @param {string} aArg.privacy The new privacy value
+ * @param {boolean} aArg.hasPrivacy Whether privacy is supported
+ * @param {string} aArg.calendarType The type of calendar
+ * @param {string[]} aArg.privacyValues The possible privacy values
+ */
+function updatePrivacy(aArg) {
+ if (aArg.hasPrivacy) {
+ // Update privacy capabilities (toolbar)
+ let menupopup = document.getElementById("event-privacy-menupopup");
+ if (menupopup) {
+ // Only update the toolbar if the button is actually there
+ for (let node of menupopup.childNodes) {
+ let currentProvider = node.getAttribute("provider");
+ if (node.hasAttribute("privacy")) {
+ let currentPrivacyValue = node.getAttribute("privacy");
+ // Collapsed state
+
+ // Hide the toolbar if the value is unsupported or is for a
+ // specific provider and doesn't belong to the current provider.
+ if (!aArg.privacyValues.includes(currentPrivacyValue) ||
+ (currentProvider && currentProvider != aArg.calendarType)) {
+ node.setAttribute("collapsed", "true");
+ } else {
+ node.removeAttribute("collapsed");
+ }
+
+ // Checked state
+ if (aArg.privacy == currentPrivacyValue) {
+ node.setAttribute("checked", "true");
+ } else {
+ node.removeAttribute("checked");
+ }
+ }
+ }
+ }
+
+ // Update privacy capabilities (menu) but only if we are not in a tab.
+ if (!gTabmail) {
+ menupopup = document.getElementById("options-privacy-menupopup");
+ for (let node of menupopup.childNodes) {
+ let currentProvider = node.getAttribute("provider");
+ if (node.hasAttribute("privacy")) {
+ let currentPrivacyValue = node.getAttribute("privacy");
+ // Collapsed state
+
+ // Hide the menu if the value is unsupported or is for a
+ // specific provider and doesn't belong to the current provider.
+ if (!aArg.privacyValues.includes(currentPrivacyValue) ||
+ (currentProvider && currentProvider != aArg.calendarType)) {
+ node.setAttribute("collapsed", "true");
+ } else {
+ node.removeAttribute("collapsed");
+ }
+
+ // Checked state
+ if (aArg.privacy == currentPrivacyValue) {
+ node.setAttribute("checked", "true");
+ } else {
+ node.removeAttribute("checked");
+ }
+ }
+ }
+ }
+
+ // Update privacy capabilities (statusbar)
+ let privacyPanel = document.getElementById("status-privacy");
+ let hasAnyPrivacyValue = false;
+ for (let node of privacyPanel.childNodes) {
+ let currentProvider = node.getAttribute("provider");
+ if (node.hasAttribute("privacy")) {
+ let currentPrivacyValue = node.getAttribute("privacy");
+
+ // Hide the panel if the value is unsupported or is for a
+ // specific provider and doesn't belong to the current provider,
+ // or is not the items privacy value
+ if (!aArg.privacyValues.includes(currentPrivacyValue) ||
+ (currentProvider && currentProvider != aArg.calendarType) ||
+ aArg.privacy != currentPrivacyValue) {
+ node.setAttribute("collapsed", "true");
+ } else {
+ node.removeAttribute("collapsed");
+ hasAnyPrivacyValue = true;
+ }
+ }
+ }
+
+ // Don't show the status panel if no valid privacy value is selected
+ if (hasAnyPrivacyValue) {
+ privacyPanel.removeAttribute("collapsed");
+ } else {
+ privacyPanel.setAttribute("collapsed", "true");
+ }
+ } else {
+ // aArg.hasPrivacy is false
+ setElementValue("button-privacy", "true", "disabled");
+ setElementValue("status-privacy", "true", "collapsed");
+ // in the tab case the menu item does not exist
+ let privacyMenuItem = document.getElementById("options-privacy-menu");
+ if (privacyMenuItem) {
+ setElementValue("options-privacy-menu", "true", "disabled");
+ }
+ }
+}
+
+/**
+ * Rotates the Priority of an item to the next value
+ * following the sequence -> Not specified -> Low -> Normal -> High ->.
+ */
+function rotatePriority() {
+ let now = gConfig.priority;
+ let next;
+ if (now <= 0 || now > 9) { // not specified -> low
+ next = 9;
+ } else if (now >= 1 && now <= 4) { // high -> not specified
+ next = 0;
+ } else if (now == 5) { // normal -> high
+ next = 1;
+ } else if (now >= 6 && now <= 9) { // low -> normal
+ next = 5;
+ }
+ editConfigState({ priority: next });
+}
+
+/**
+ * Handler to change the priority.
+ *
+ * @param {nsIDOMNode} aTarget Has the new priority in its "value" attribute
+ */
+function editPriority(aTarget) {
+ let newPriority = parseInt(aTarget.getAttribute("value"), 10);
+ editConfigState({ priority: newPriority });
+}
+
+/**
+ * Updates the dialog controls related to priority.
+ *
+ * @param {Object} aArg Contains priority properties
+ * @param {string} aArg.priority The new priority value
+ * @param {boolean} aArg.hasPriority Whether priority is supported
+ */
+function updatePriority(aArg) {
+ // Set up capabilities
+ if (document.getElementById("button-priority")) {
+ setElementValue("button-priority", !aArg.hasPriority && "true", "disabled");
+ }
+ if (!gTabmail && document.getElementById("options-priority-menu")) {
+ setElementValue("options-priority-menu", !aArg.hasPriority && "true", "disabled");
+ }
+ setElementValue("status-priority", !aArg.hasPriority && "true", "collapsed");
+
+ if (aArg.hasPriority) {
+ let priorityLevel = "none";
+ if (aArg.priority >= 1 && aArg.priority <= 4) {
+ priorityLevel = "high";
+ } else if (aArg.priority == 5) {
+ priorityLevel = "normal";
+ } else if (aArg.priority >= 6 && aArg.priority <= 9) {
+ priorityLevel = "low";
+ }
+
+ let priorityNone = document.getElementById("cmd_priority_none");
+ let priorityLow = document.getElementById("cmd_priority_low");
+ let priorityNormal = document.getElementById("cmd_priority_normal");
+ let priorityHigh = document.getElementById("cmd_priority_high");
+
+ priorityNone.setAttribute("checked",
+ priorityLevel == "none" ? "true" : "false");
+ priorityLow.setAttribute("checked",
+ priorityLevel == "low" ? "true" : "false");
+ priorityNormal.setAttribute("checked",
+ priorityLevel == "normal" ? "true" : "false");
+ priorityHigh.setAttribute("checked",
+ priorityLevel == "high" ? "true" : "false");
+
+ // Status bar panel
+ let priorityPanel = document.getElementById("status-priority");
+ if (priorityLevel == "none") {
+ // If the priority is none, don't show the status bar panel
+ priorityPanel.setAttribute("collapsed", "true");
+ } else {
+ priorityPanel.removeAttribute("collapsed");
+ let foundPriority = false;
+ for (let node of priorityPanel.childNodes) {
+ if (foundPriority) {
+ node.setAttribute("collapsed", "true");
+ } else {
+ node.removeAttribute("collapsed");
+ }
+ if (node.getAttribute("value") == priorityLevel) {
+ foundPriority = true;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Rotate the Status of an item to the next value following
+ * the sequence -> NONE -> TENTATIVE -> CONFIRMED -> CANCELLED ->.
+ */
+function rotateStatus() {
+ let oldStatus = gConfig.status;
+ let noneCommand = document.getElementById("cmd_status_none");
+ let noneCommandIsVisible = !noneCommand.hasAttribute("hidden");
+ let states = ["TENTATIVE", "CONFIRMED", "CANCELLED"];
+
+ // If control for status "NONE" ("cmd_status_none") is visible,
+ // allow rotating to it.
+ if (gConfig.isEvent && noneCommandIsVisible) {
+ states.unshift("NONE");
+ }
+
+ let newStatus = states[(states.indexOf(oldStatus) + 1) % states.length];
+ editConfigState({ status: newStatus });
+}
+
+/**
+ * Handler for changing the status.
+ *
+ * @param {nsIDOMNode} aTarget Has the new status in its "value" attribute
+ */
+function editStatus(aTarget) {
+ let newStatus = aTarget.getAttribute("value");
+ editConfigState({ status: newStatus });
+}
+
+/**
+ * Update the dialog controls related to status.
+ *
+ * @param {Object} aArg Contains the new status value
+ * @param {string} aArg.status The new status value
+ */
+function updateStatus(aArg) {
+ const statusLabels = ["status-status-tentative-label",
+ "status-status-confirmed-label",
+ "status-status-cancelled-label"];
+ const commands = ["cmd_status_none",
+ "cmd_status_tentative",
+ "cmd_status_confirmed",
+ "cmd_status_cancelled"];
+ let found = false;
+ setBooleanAttribute("status-status", "collapsed", true);
+ commands.forEach((aElement, aIndex, aArray) => {
+ let node = document.getElementById(aElement);
+ let matches = (node.getAttribute("value") == aArg.status);
+ found = found || matches;
+
+ node.setAttribute("checked", matches ? "true" : "false");
+
+ if (aIndex > 0) {
+ setBooleanAttribute(statusLabels[aIndex-1], "hidden", !matches);
+ if (matches) {
+ setBooleanAttribute("status-status", "collapsed", false);
+ }
+ }
+ });
+ if (!found) {
+ // The current Status value is invalid. Change the status to
+ // "not specified" and update the status again.
+ sendMessage({ command: "editStatus", value: "NONE" });
+ }
+}
+
+/**
+ * Toggles the transparency ("Show Time As" property) of an item
+ * from BUSY (Opaque) to FREE (Transparent).
+ */
+function rotateShowTimeAs() {
+ const states = ["OPAQUE", "TRANSPARENT"];
+ let oldValue = gConfig.showTimeAs;
+ let newValue = states[(states.indexOf(oldValue) + 1) % states.length];
+ editConfigState({ showTimeAs: newValue });
+}
+
+/**
+ * Handler for changing the transparency.
+ *
+ * @param {nsIDOMNode} aTarget Has the new transparency in its "value" attribute
+ */
+function editShowTimeAs(aTarget) {
+ let newValue = aTarget.getAttribute("value");
+ editConfigState({ showTimeAs: newValue });
+}
+
+/**
+ * Update the dialog controls related to transparency.
+ *
+ * @param {Object} aArg Contains the new transparency value
+ * @param {string} aArg.showTimeAs The new transparency value
+ */
+function updateShowTimeAs(aArg) {
+ let showAsBusy = document.getElementById("cmd_showtimeas_busy");
+ let showAsFree = document.getElementById("cmd_showtimeas_free");
+
+ showAsBusy.setAttribute("checked",
+ aArg.showTimeAs == "OPAQUE" ? "true" : "false");
+ showAsFree.setAttribute("checked",
+ aArg.showTimeAs == "TRANSPARENT" ? "true" : "false");
+
+ setBooleanAttribute("status-freebusy",
+ "collapsed",
+ aArg.showTimeAs != "OPAQUE" && aArg.showTimeAs != "TRANSPARENT");
+ setBooleanAttribute("status-freebusy-free-label", "hidden", aArg.showTimeAs == "OPAQUE");
+ setBooleanAttribute("status-freebusy-busy-label", "hidden", aArg.showTimeAs == "TRANSPARENT");
+}
+
+/**
+ * Change the task percent complete (and thus task status).
+ *
+ * @param {short} aPercentComplete The new percent complete value
+ */
+function editToDoStatus(aPercentComplete) {
+ sendMessage({ command: "editToDoStatus", value: aPercentComplete });
+}
+
+/**
+ * Check or uncheck the "Mark updated" menu item in "Events and Tasks"
+ * menu based on the percent complete value. (The percent complete menu
+ * items are updated by changeMenuByPropertyName in calendar-menus.xml)
+ *
+ * @param {Object} aArg Container
+ * @param {short} aArg.percentComplete The percent complete value
+ */
+function updateMarkCompletedMenuItem(aArg) {
+ // Command only for tab case, function only to be executed in dialog windows.
+ if (gTabmail) {
+ let completedCommand = document.getElementById("calendar_toggle_completed_command");
+ let isCompleted = aArg.percentComplete == 100;
+ completedCommand.setAttribute("checked", isCompleted);
+ }
+}
+
+/**
+ * Postpone the task's start date/time and due date/time. ISO 8601
+ * format: "PT1H", "P1D", and "P1W" are 1 hour, 1 day, and 1 week. (We
+ * use this format intentionally instead of a calIDuration object because
+ * those objects cannot be serialized for message passing with iframes.)
+ *
+ * @param {string} aDuration A duration in ISO 8601 format
+ */
+function postponeTask(aDuration) {
+ sendMessage({ command: "postponeTask", value: aDuration });
+}
+
+/**
+ * Get the timezone button state.
+ *
+ * @return {boolean} True is active/checked and false is inactive/unchecked
+ */
+function getTimezoneCommandState() {
+ let cmdTimezone = document.getElementById("cmd_timezone");
+ return cmdTimezone.getAttribute("checked") == "true";
+}
+
+/**
+ * Set the timezone button state. Used to keep the toolbar button in
+ * sync when switching tabs.
+ *
+ * @param {Object} aArg Contains timezones property
+ * @param {boolean} aArg.timezonesEnabled Are timezones enabled?
+ */
+function updateTimezoneCommand(aArg) {
+ let cmdTimezone = document.getElementById("cmd_timezone");
+ cmdTimezone.setAttribute("checked", aArg.timezonesEnabled);
+ gConfig.timezonesEnabled = aArg.timezonesEnabled;
+}
+
+/**
+ * Toggles the command that allows enabling the timezone links in the dialog.
+ */
+function toggleTimezoneLinks() {
+ let cmdTimezone = document.getElementById("cmd_timezone");
+ let currentState = getTimezoneCommandState();
+ cmdTimezone.setAttribute("checked", currentState ? "false" : "true");
+ gConfig.timezonesEnabled = !currentState;
+ sendMessage({ command: "toggleTimezoneLinks", checked: !currentState });
+}
+
+/**
+ * Toggles the visibility of the related link (rfc2445 URL property).
+ */
+function toggleLink() {
+ let linkCommand = document.getElementById("cmd_toggle_link");
+ let checked = linkCommand.getAttribute("checked") == "true";
+
+ linkCommand.setAttribute("checked", checked ? "false" : "true");
+ sendMessage({ command: "toggleLink", checked: !checked });
+}
+
+/**
+ * Prompts the user to attach an url to this item.
+ */
+function attachURL() {
+ sendMessage({ command: "attachURL" });
+}
+
+/**
+ * Updates dialog controls related to item attachments.
+ *
+ * @param {Object} aArg Container
+ * @param {boolean} aArg.attachUrlCommand Enable the attach url command?
+ */
+function updateAttachment(aArg) {
+ setElementValue("cmd_attach_url", !aArg.attachUrlCommand && "true", "disabled");
+}
+
+/**
+ * Updates attendees command enabled/disabled state.
+ *
+ * @param {Object} aArg Container
+ * @param {boolean} aArg.attendeesCommand Enable the attendees command?
+ */
+function updateAttendeesCommand(aArg) {
+ setElementValue("cmd_attendees", !aArg.attendeesCommand, "disabled");
+}
+
+/**
+ * Enables/disables the commands cmd_accept and cmd_save related to the
+ * save operation.
+ *
+ * @param {boolean} aEnable Enable the commands?
+ */
+function enableAcceptCommand(aEnable) {
+ setElementValue("cmd_accept", !aEnable, "disabled");
+ setElementValue("cmd_save", !aEnable, "disabled");
+}
+
+/**
+ * Enable and un-collapse all elements that are disable-on-readonly and
+ * collapse-on-readonly.
+ */
+function removeDisableAndCollapseOnReadonly() {
+ let enableElements = document.getElementsByAttribute("disable-on-readonly", "true");
+ for (let element of enableElements) {
+ element.removeAttribute("disabled");
+ }
+ let collapseElements = document.getElementsByAttribute("collapse-on-readonly", "true");
+ for (let element of collapseElements) {
+ element.removeAttribute("collapsed");
+ }
+}
+
+/**
+ * Handler to toggle toolbar visibility.
+ *
+ * @param {string} aToolbarId The id of the toolbar node to toggle
+ * @param {string} aMenuitemId The corresponding menuitem in the view menu
+ */
+function onCommandViewToolbar(aToolbarId, aMenuItemId) {
+ let toolbar = document.getElementById(aToolbarId);
+ let menuItem = document.getElementById(aMenuItemId);
+
+ if (!toolbar || !menuItem) {
+ return;
+ }
+
+ let toolbarCollapsed = toolbar.collapsed;
+
+ // toggle the checkbox
+ menuItem.setAttribute("checked", toolbarCollapsed);
+
+ // toggle visibility of the toolbar
+ toolbar.collapsed = !toolbarCollapsed;
+
+ document.persist(aToolbarId, "collapsed");
+ document.persist(aMenuItemId, "checked");
+}
+
+/**
+ * Called after the customize toolbar dialog has been closed by the
+ * user. We need to restore the state of all buttons and commands of
+ * all customizable toolbars.
+ *
+ * @param {boolean} aToolboxChanged When true the toolbox has changed
+ */
+function dialogToolboxCustomizeDone(aToolboxChanged) {
+ // Re-enable menu items (disabled during toolbar customization).
+ let menubarId = gTabmail ? "mail-menubar" : "event-menubar";
+ let menubar = document.getElementById(menubarId);
+ for (let menuitem of menubar.childNodes) {
+ menuitem.removeAttribute("disabled");
+ }
+
+ // make sure our toolbar buttons have the correct enabled state restored to them...
+ document.commandDispatcher.updateCommands("itemCommands");
+
+ // Enable the toolbar context menu items
+ document.getElementById("cmd_customize").removeAttribute("disabled");
+
+ // Update privacy items to make sure the toolbarbutton's menupopup is set
+ // correctly
+ updatePrivacy(gConfig);
+}
+
+/**
+ * Handler to start the customize toolbar dialog for the event dialog's toolbar.
+ */
+function onCommandCustomize() {
+ // install the callback that handles what needs to be
+ // done after a toolbar has been customized.
+ let toolboxId = "event-toolbox";
+
+ let toolbox = document.getElementById(toolboxId);
+ toolbox.customizeDone = dialogToolboxCustomizeDone;
+
+ // Disable menu items during toolbar customization.
+ let menubarId = gTabmail ? "mail-menubar" : "event-menubar";
+ let menubar = document.getElementById(menubarId);
+ for (let menuitem of menubar.childNodes) {
+ menuitem.setAttribute("disabled", true);
+ }
+
+ // Disable the toolbar context menu items
+ document.getElementById("cmd_customize").setAttribute("disabled", "true");
+
+ let wintype = document.documentElement.getAttribute("windowtype");
+ wintype = wintype.replace(/:/g, "");
+
+ window.openDialog("chrome://global/content/customizeToolbar.xul",
+ "CustomizeToolbar" + wintype,
+ "chrome,all,dependent",
+ document.getElementById(toolboxId), // toolbox dom node
+ false, // is mode toolbar yes/no?
+ null, // callback function
+ "dialog"); // name of this mode
+}
+
+/**
+ * Add menu items to the UI for attaching files using a cloud provider.
+ *
+ * @param {Object[]} aItemObjects Array of objects that each contain
+ * data to create a menuitem
+ */
+function loadCloudProviders(aItemObjects) {
+ /**
+ * Deletes any existing menu items in aParentNode that have a
+ * cloudProviderAccountKey attribute.
+ *
+ * @param {nsIDOMNode} aParentNode A menupopup containing menu items
+ */
+ function deleteAlreadyExisting(aParentNode) {
+ for (let node of aParentNode.childNodes) {
+ if (node.cloudProviderAccountKey) {
+ aParentNode.removeChild(node);
+ }
+ }
+ }
+
+ // Delete any existing menu items with a cloudProviderAccountKey,
+ // needed for the tab case to prevent duplicate menu items, and
+ // helps keep the menu items current.
+ let toolbarPopup = document.getElementById("button-attach-menupopup");
+ if (toolbarPopup) {
+ deleteAlreadyExisting(toolbarPopup);
+ }
+ let optionsPopup = document.getElementById("options-attachments-menupopup");
+ if (optionsPopup) {
+ deleteAlreadyExisting(optionsPopup);
+ }
+
+ for (let itemObject of aItemObjects) {
+ // Create a menu item.
+ let item = createXULElement("menuitem");
+ item.setAttribute("label", itemObject.label);
+ item.setAttribute("observes", "cmd_attach_cloud");
+ item.setAttribute("oncommand", "attachFileByAccountKey(event.target.cloudProviderAccountKey); event.stopPropagation();");
+
+ if (itemObject.class) {
+ item.setAttribute("class", itemObject.class);
+ item.setAttribute("image", itemObject.image);
+ }
+
+ // Add the menu item to the UI.
+ if (toolbarPopup) {
+ toolbarPopup.appendChild(item.cloneNode(true)).cloudProviderAccountKey = itemObject.cloudProviderAccountKey;
+ }
+ if (optionsPopup) {
+ // This one doesn't need to clone, just use the item itself.
+ optionsPopup.appendChild(item).cloudProviderAccountKey = itemObject.cloudProviderAccountKey;
+ }
+ }
+}
+
+/**
+ * Send a message to attach a file using a given cloud provider,
+ * to be identified by the cloud provider's accountKey.
+ *
+ * @param {string} aAccountKey The accountKey for a cloud provider
+ */
+function attachFileByAccountKey(aAccountKey) {
+ sendMessage({ command: "attachFileByAccountKey", accountKey: aAccountKey });
+}
diff --git a/calendar/lightning/content/lightning-item-panel.xul b/calendar/lightning/content/lightning-item-panel.xul
new file mode 100644
index 000000000..810c52ccf
--- /dev/null
+++ b/calendar/lightning/content/lightning-item-panel.xul
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- lightning-toolbar.dtd is only needed for the app menu button -->
+<!DOCTYPE overlay [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ <!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd">
+ <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd">
+ <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd">
+ <!ENTITY % toolbarDTD SYSTEM "chrome://lightning/locale/lightning-toolbar.dtd">
+ %brandDTD;
+ %globalDTD;
+ %calendarDTD;
+ %eventDialogDTD;
+ %toolbarDTD;
+]>
+
+<?xul-overlay href="chrome://lightning/content/lightning-item-toolbar.xul"?>
+
+<overlay id="ltnCalendarItemPanelContentOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <tabpanels id="tabpanelcontainer">
+ <vbox id="lightningItemPanel" collapsed="true">
+
+ <!-- The id of the inner vbox and the iframe are set dynamically
+ when a tab is created. -->
+ <vbox flex="1"
+ id="dummy-calendar-event-dialog-tab"
+ class="calendar-event-dialog-tab">
+
+ <!-- Commands -->
+ <commandset id="itemCommands">
+ <command id="cmd_save"
+ disable-on-readonly="true"
+ oncommand="onCommandSave()"/>
+ <command id="cmd_item_delete"
+ disable-on-readonly="true"
+ oncommand="onCommandDeleteItem()"/>
+
+ <!-- View menu -->
+ <command id="cmd_customize"
+ oncommand="onCommandCustomize()"/>
+ <command id="cmd_toggle_link"
+ persist="checked"
+ oncommand="toggleLink()"/>
+
+ <!-- status -->
+ <command id="cmd_status_none"
+ oncommand="editStatus(event.target)"
+ hidden="true"
+ value="NONE"/>
+ <command id="cmd_status_tentative"
+ oncommand="editStatus(event.target)"
+ value="TENTATIVE"/>
+ <command id="cmd_status_confirmed"
+ oncommand="editStatus(event.target)"
+ value="CONFIRMED"/>
+ <command id="cmd_status_cancelled"
+ oncommand="editStatus(event.target)"
+ value="CANCELLED"/>
+
+ <!-- priority -->
+ <command id="cmd_priority_none"
+ oncommand="editPriority(event.target)"
+ value="0"/>
+ <command id="cmd_priority_low"
+ oncommand="editPriority(event.target)"
+ value="9"/>
+ <command id="cmd_priority_normal"
+ oncommand="editPriority(event.target)"
+ value="5"/>
+ <command id="cmd_priority_high"
+ oncommand="editPriority(event.target)"
+ value="1"/>
+
+ <!-- freebusy -->
+ <command id="cmd_showtimeas_busy"
+ oncommand="editShowTimeAs(event.target)"
+ value="OPAQUE"/>
+ <command id="cmd_showtimeas_free"
+ oncommand="editShowTimeAs(event.target)"
+ value="TRANSPARENT"/>
+
+ <!-- attendees -->
+ <command id="cmd_attendees"
+ oncommand="editAttendees();"/>
+
+ <!-- accept, attachments, timezone -->
+ <command id="cmd_accept"
+ disable-on-readonly="true"
+ oncommand="onAccept();"/>
+ <command id="cmd_attach_url"
+ disable-on-readonly="true"
+ oncommand="attachURL()"/>
+ <command id="cmd_attach_cloud"
+ disable-on-readonly="true"/>
+ <command id="cmd_timezone"
+ persist="checked"
+ checked="false"
+ oncommand="toggleTimezoneLinks()"/>
+ </commandset>
+
+ <keyset id="calendar-event-dialog-keyset">
+ <key id="save-key"
+ modifiers="accel, shift"
+ key="&event.dialog.save.key;"
+ command="cmd_save"/>
+ <key id="saveandclose-key"
+ modifiers="accel"
+ key="&event.dialog.saveandclose.key;"
+ command="cmd_accept"/>
+ <key id="saveandclose-key2"
+ modifiers="accel"
+ keycode="VK_RETURN"
+ command="cmd_accept"/>
+ </keyset>
+
+ <toolbox id="event-toolbox"
+ class="mail-toolbox"
+ mode="full"
+ defaultmode="full"
+ labelalign="end"
+ defaultlabelalign="end">
+ <!-- more toolbarpalette items are added with an overlay -->
+ <toolbarpalette id="event-toolbarpalette">
+ <toolbarbutton id="calendar-item-appmenu-button"
+ class="toolbarbutton-1 button-appmenu"
+ label="&lightning.toolbar.appmenuButton.label;"
+ tooltiptext="&lightning.toolbar.appmenuButton1.tooltip;"/>
+ </toolbarpalette>
+ <!-- toolboxid is set here since we move the toolbar around for tabs -->
+ <toolbar id="event-tab-toolbar"
+ toolbarname="&event.menu.view.toolbars.event.label;"
+ accesskey="&event.menu.view.toolbars.event.accesskey;"
+ toolboxid="event-toolbox"
+ class="chromeclass-toolbar inline-toolbar"
+ customizable="true"
+ labelalign="end"
+ defaultlabelalign="end"
+ context="event-dialog-toolbar-context-menu"
+ defaultset="button-saveandclose,button-attendees,button-privacy,button-url,button-priority,button-status,button-freebusy,button-delete,spring,calendar-item-appmenu-button"/>
+ <toolbarset id="custom-toolbars" context="event-dialog-toolbar-context-menu"/>
+ </toolbox>
+
+ <iframe id="lightning-item-panel-iframe" flex="1"/>
+
+ </vbox>
+ </vbox>
+ </tabpanels>
+
+ <popupset id="calendar-popupset">
+ <menupopup id="event-dialog-toolbar-context-menu"
+ onpopupshowing="onToolbarsPopupShowingForTabType(event);">
+ <menuseparator id="customizeEventToolbarMenuSeparator"/>
+ <menuitem id="CustomizeDialogToolbar"
+ label="&event.menu.view.toolbars.customize.label;"
+ command="cmd_customize"/>
+ </menupopup>
+ </popupset>
+
+</overlay>
+
diff --git a/calendar/lightning/content/lightning-item-toolbar.xul b/calendar/lightning/content/lightning-item-toolbar.xul
new file mode 100644
index 000000000..982df7ecb
--- /dev/null
+++ b/calendar/lightning/content/lightning-item-toolbar.xul
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ <!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd">
+ <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd">
+ <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd">
+ %brandDTD;
+ %globalDTD;
+ %calendarDTD;
+ %eventDialogDTD;
+]>
+
+<overlay id="ltnCalendarItemPanelContentOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <toolbarpalette id="event-toolbarpalette">
+ <toolbarbutton id="button-save"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ label="&event.toolbar.save.label2;"
+ tooltiptext="&event.toolbar.save.tooltip2;"
+ command="cmd_save"/>
+ <toolbarbutton id="button-saveandclose"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ label="&event.toolbar.saveandclose.label;"
+ tooltiptext="&event.toolbar.saveandclose.tooltip;"
+ command="cmd_accept"/>
+ <toolbarbutton id="button-attendees"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1 event-only"
+ disable-on-readonly="true"
+ label="&event.toolbar.attendees.label;"
+ tooltiptext="&event.toolbar.attendees.tooltip;"
+ command="cmd_attendees"/>
+ <toolbarbutton id="button-privacy"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ type="menu-button"
+ disable-on-readonly="true"
+ label="&event.toolbar.privacy.label;"
+ tooltiptext="&event.toolbar.privacy.tooltip;"
+ oncommand="rotatePrivacy()">
+ <menupopup id="event-privacy-menupopup">
+ <menuitem id="event-privacy-public-menuitem"
+ name="event-privacy-group"
+ label="&event.menu.options.privacy.public.label;"
+ type="radio"
+ privacy="PUBLIC"
+ oncommand="editPrivacy(this, event)"/>
+ <menuitem id="event-privacy-confidential-menuitem"
+ name="event-privacy-group"
+ label="&event.menu.options.privacy.confidential.label;"
+ type="radio"
+ privacy="CONFIDENTIAL"
+ oncommand="editPrivacy(this, event)"/>
+ <menuitem id="event-privacy-private-menuitem"
+ name="event-privacy-group"
+ label="&event.menu.options.privacy.private.label;"
+ type="radio"
+ privacy="PRIVATE"
+ oncommand="editPrivacy(this, event)"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-url"
+ type="menu-button"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ label="&event.attachments.menubutton.label;"
+ tooltiptext="&event.toolbar.attachments.tooltip;"
+ command="cmd_attach_url"
+ disable-on-readonly="true">
+ <menupopup id="button-attach-menupopup">
+ <menuitem id="button-attach-url"
+ label="&event.attachments.url.label;"
+ command="cmd_attach_url"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-delete"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ label="&event.toolbar.delete.label;"
+ tooltiptext="&event.toolbar.delete.tooltip;"
+ command="cmd_item_delete"
+ disable-on-readonly="true"/>
+ <toolbarbutton id="button-priority"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ type="menu-button"
+ disable-on-readonly="true"
+ label="&event.menu.options.priority2.label;"
+ tooltiptext="&event.toolbar.priority.tooltip;"
+ oncommand="rotatePriority()">
+ <menupopup id="event-priority-menupopup">
+ <menuitem id="event-priority-none-menuitem"
+ name="event-priority-group"
+ label="&event.menu.options.priority.notspecified.label;"
+ type="radio"
+ command="cmd_priority_none"/>
+ <menuitem id="event-priority-low-menuitem"
+ name="event-priority-group"
+ label="&event.menu.options.priority.low.label;"
+ type="radio"
+ command="cmd_priority_low"/>
+ <menuitem id="event-priority-normal-menuitem"
+ name="event-priority-group"
+ label="&event.menu.options.priority.normal.label;"
+ type="radio"
+ command="cmd_priority_normal"/>
+ <menuitem id="event-priority-high-menuitem"
+ name="event-priority-group"
+ label="&event.menu.options.priority.high.label;"
+ type="radio"
+ command="cmd_priority_high"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-status"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1 event-only"
+ type="menu-button"
+ disable-on-readonly="true"
+ label="&newevent.status.label;"
+ tooltiptext="&event.toolbar.status.tooltip;"
+ oncommand="rotateStatus()">
+ <menupopup id="event-status-menupopup">
+ <menuitem id="event-status-none-menuitem"
+ name="event-status-group"
+ label="&newevent.eventStatus.none.label;"
+ type="radio"
+ command="cmd_status_none"/>
+ <menuitem id="event-status-tentative-menuitem"
+ name="event-status-group"
+ label="&newevent.status.tentative.label;"
+ type="radio"
+ command="cmd_status_tentative"/>
+ <menuitem id="event-status-confirmed-menuitem"
+ name="event-status-group"
+ label="&newevent.status.confirmed.label;"
+ type="radio"
+ command="cmd_status_confirmed"/>
+ <menuitem id="event-status-cancelled-menuitem"
+ name="event-status-group"
+ label="&newevent.eventStatus.cancelled.label;"
+ type="radio"
+ command="cmd_status_cancelled"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-freebusy"
+ mode="dialog"
+ class="cal-event-toolbarbutton toolbarbutton-1 event-only"
+ type="menu-button"
+ disable-on-readonly="true"
+ label="&event.menu.options.show.time.label;"
+ tooltiptext="&event.toolbar.freebusy.tooltip;"
+ oncommand="rotateShowTimeAs()">
+ <menupopup id="event-freebusy-menupopup">
+ <menuitem id="event-freebusy-busy-menuitem"
+ name="event-freebusy-group"
+ label="&event.menu.options.show.time.busy.label;"
+ type="radio"
+ command="cmd_showtimeas_busy"/>
+ <menuitem id="event-freebusy-free-menuitem"
+ name="event-freebusy-group"
+ label="&event.menu.options.show.time.free.label;"
+ type="radio"
+ command="cmd_showtimeas_free"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-timezones"
+ mode="dialog"
+ type="checkbox"
+ class="cal-event-toolbarbutton toolbarbutton-1"
+ label="&event.menu.options.timezone2.label;"
+ tooltiptext="&event.menu.options.timezone2.label;"
+ command="cmd_timezone"/>
+ </toolbarpalette>
+</overlay>
diff --git a/calendar/lightning/content/lightning-menus.xul b/calendar/lightning/content/lightning-menus.xul
new file mode 100644
index 000000000..9fdebb9ac
--- /dev/null
+++ b/calendar/lightning/content/lightning-menus.xul
@@ -0,0 +1,814 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd"> %lightningDTD;
+ <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd" > %calendarDTD;
+ <!ENTITY % toolbarDTD SYSTEM "chrome://lightning/locale/lightning-toolbar.dtd" > %toolbarDTD;
+ <!ENTITY % menuOverlayDTD SYSTEM "chrome://calendar/locale/menuOverlay.dtd" > %menuOverlayDTD;
+ <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd" > %eventDialogDTD;
+]>
+
+<overlay id="ltnMenusOverlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <menupopup id="menu_FilePopup">
+ <menu id="menu_Open"
+ mode="calendar"
+ label="&lightning.menupopup.open.label;"
+ accesskey="&lightning.menupopup.open.accesskey;"
+ insertafter="menu_New">
+ <menupopup id="menu_OpenPopup">
+ <menuitem id="ltnOpenMessageFileMenuitem"
+ label="&lightning.menupopup.open.message.label;"
+ accesskey="&lightning.menupopup.open.message.accesskey;"
+ oncommand="MsgOpenFromFile();"/>
+ <menuitem id="ltnOpenCalendarFileMenuitem"
+ label="&lightning.menupopup.open.calendar.label;"
+ accesskey="&lightning.menupopup.open.calendar.accesskey;"
+ oncommand="openLocalCalendar();"/>
+ </menupopup>
+ </menu>
+ <menuitem id="ltnSave"
+ insertbefore="menu_saveAs"
+ label="&event.menu.item.save.label;"
+ accesskey="&event.menu.item.save.tab.accesskey;"
+ key="save-key"
+ command="cmd_save"
+ observes="cmd_save"/>
+ <menuitem id="ltnSaveAndClose"
+ insertafter="ltnSave"
+ label="&event.menu.item.saveandclose.label;"
+ accesskey="&event.menu.item.saveandclose.tab.accesskey;"
+ command="cmd_accept"
+ observes="cmd_accept"/>
+ </menupopup>
+ <menuitem id="openMessageFileMenuitem" hidden="true"/>
+
+ <menupopup id="menu_NewPopup">
+ <menuitem id="ltnNewEvent"
+ label="&lightning.menupopup.new.event.label;"
+ insertbefore="menu_newFolder"
+ accesskey="&lightning.menupopup.new.event.accesskey;"
+ key="calendar-new-event-key"
+ command="calendar_new_event_command"
+ observes="calendar_new_event_command"/>
+ <menuitem id="ltnNewTask"
+ label="&lightning.menupopup.new.task.label;"
+ insertbefore="menu_newFolder"
+ accesskey="&lightning.menupopup.new.task.accesskey;"
+ key="calendar-new-todo-key"
+ command="calendar_new_todo_command"
+ observes="calendar_new_todo_command"/>
+ <menuseparator id="afterltnNewTask"
+ insertbefore="menu_newFolder"
+ observes="menu_newFolder"/>
+ <menuitem id="ltnNewCalendar" label="&lightning.menupopup.new.calendar.label;"
+ command="calendar_new_calendar_command"
+ observes="calendar_new_calendar_command"
+ accesskey="&lightning.menupopup.new.calendar.accesskey;"
+ insertafter="newAccountMenuItem"/>
+ </menupopup>
+
+ <menupopup id="menu_EditPopup">
+ <menuitem id="ltnCalendarProperties"
+ insertafter="menu_properties"
+ label="&calendar.properties.label;"
+ accesskey="&calendar.properties.accesskey;"
+ command="calendar_edit_calendar_command"
+ observes="calendar_edit_calendar_command"/>
+ </menupopup>
+
+ <menupopup id="menu_View_Popup">
+ <menu id="menu_Toolbars"
+ onpopupshowing="onToolbarsPopupShowingForTabType(event);"/>
+ <menuseparator id="ltnViewMenuSeparator"
+ insertbefore="viewSortMenuSeparator"/>
+ <menu id="ltnTodayPaneMenu"
+ insertbefore="viewSortMenuSeparator"
+ label="&calendar.context.button.label;"
+ accesskey="&calendar.context.button.accesskey;">
+ <menupopup id="ltnTodayPaneMenuPopup">
+ <menuitem id="ltnShowTodayPane-2"
+ label="&todaypane.showTodayPane.label;"
+ accesskey="&todaypane.showTodayPane.accesskey;"
+ type="checkbox"
+ key="todaypanekey"
+ command="calendar_toggle_todaypane_command"/>
+ <menuseparator id="ltnSeparatorBeforeDisplayMiniday"/>
+ <menuitem id="ltnTodayPaneDisplayMiniday"
+ name="minidisplay"
+ value="miniday"
+ type="radio"
+ oncommand="TodayPane.displayMiniSection('miniday')"
+ label="&todaypane.showMiniday.label;"
+ accesskey="&todaypane.showMiniday.accesskey;"/>
+ <menuitem id="ltnTodayPaneDisplayMinimonth"
+ name="minidisplay"
+ value="minimonth"
+ type="radio"
+ oncommand="TodayPane.displayMiniSection('minimonth')"
+ label="&todaypane.showMinimonth.label;"
+ accesskey="&todaypane.showMinimonth.accesskey;"/>
+ <menuitem id="ltnTodayPaneDisplayNone"
+ name="minidisplay"
+ value="none"
+ type="radio"
+ oncommand="TodayPane.displayMiniSection('none')"
+ label="&todaypane.showNone.label;"
+ accesskey="&todaypane.showNone.accesskey;"/>
+ </menupopup>
+ </menu>
+ <menu id="ltnCalendarMenu"
+ observes="calendar_in_foreground"
+ insertbefore="viewSortMenuSeparator"
+ label="&lightning.menu.view.calendar.label;"
+ accesskey="&lightning.menu.view.calendar.accesskey;">
+ <menupopup id="ltnCalendarMenuPopup">
+ <menuitem id="ltnChangeViewDay"
+ label="&lightning.toolbar.day.label;"
+ accesskey="&lightning.toolbar.day.accesskey;"
+ type="radio"
+ name="calendarMenuViews"
+ key="calendar-day-view-key"
+ command="calendar_day-view_command"/>
+ <menuitem id="ltnChangeViewWeek"
+ label="&lightning.toolbar.week.label;"
+ accesskey="&lightning.toolbar.week.accesskey;"
+ type="radio"
+ name="calendarMenuViews"
+ key="calendar-week-view-key"
+ command="calendar_week-view_command"/>
+ <menuitem id="ltnChangeViewMultiweek"
+ label="&lightning.toolbar.multiweek.label;"
+ accesskey="&lightning.toolbar.multiweek.accesskey;"
+ type="radio"
+ name="calendarMenuViews"
+ key="calendar-multiweek-view-key"
+ command="calendar_multiweek-view_command"/>
+ <menuitem id="ltnChangeViewMonth"
+ label="&lightning.toolbar.month.label;"
+ accesskey="&lightning.toolbar.month.accesskey;"
+ type="radio"
+ name="calendarMenuViews"
+ key="calendar-month-view-key"
+ command="calendar_month-view_command"/>
+ <menuseparator id="ltnBeforeCalendarViewSection"/>
+ <menu id="ltnCalendarPaneMenu"
+ label="&lightning.toolbar.calendarmenu.label;"
+ accesskey="&lightning.toolbar.calendarmenu.accesskey;">
+ <menupopup id="ltnCalendarPanePopup"
+ onpopupshowing="InitViewCalendarPaneMenu()">
+ <menuitem id="ltnViewCalendarPane"
+ type="checkbox"
+ label="&lightning.toolbar.calendarpane.label;"
+ accesskey="&lightning.toolbar.calendarpane.accesskey;"
+ command="calendar_toggle_calendarsidebar_command"/>
+ <menuseparator id="ltnCalendarPaneMenuSeparator"/>
+ <menuitem id="ltnTasksViewMinimonth"
+ type="checkbox"
+ label="&calendar.tasks.view.minimonth.label;"
+ accesskey="&calendar.tasks.view.minimonth.accesskey;"
+ command="calendar_toggle_minimonthpane_command"/>
+ <menuitem id="ltnTasksViewCalendarlist"
+ type="checkbox"
+ label="&calendar.tasks.view.calendarlist.label;"
+ accesskey="&calendar.tasks.view.calendarlist.accesskey;"
+ command="calendar_toggle_calendarlist_command"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="ltnBeforeCurrentViewMenu"/>
+ <menu id="ltnCalendarCurrentViewMenu"
+ observes="calendar_mode_calendar"
+ label="&showCurrentView.label;"
+ accesskey="&showCurrentView.accesskey;">
+ <menupopup id="ltnCalendarCurrentViewMenuPopup">
+ <menuitem type="checkbox"
+ id="ltnWorkdaysOnlyMenuitem"
+ label="&calendar.onlyworkday.checkbox.label;"
+ accesskey="&calendar.onlyworkday.checkbox.accesskey;"
+ observes="calendar_toggle_workdays_only_command"/>
+ <menuitem type="checkbox"
+ id="ltnTasksInViewMenuitem"
+ label="&calendar.displaytodos.checkbox.label;"
+ accesskey="&calendar.displaytodos.checkbox.accesskey;"
+ observes="calendar_toggle_tasks_in_view_command"/>
+ <menuitem type="checkbox"
+ id="ltnShowCompletedInViewMenuItem"
+ label="&calendar.completedtasks.checkbox.label;"
+ accesskey="&calendar.completedtasks.checkbox.accesskey;"
+ observes="calendar_toggle_show_completed_in_view_command"/>
+ <menuitem type="checkbox"
+ id="ltnViewRotated"
+ label="&calendar.orientation.label;"
+ accesskey="&calendar.orientation.accesskey;"
+ command="calendar_toggle_orientation_command"
+ observes="calendar_toggle_orientation_command"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ <menu id="ltnTasksMenu"
+ observes="calendar_mode_task"
+ insertbefore="viewSortMenuSeparator"
+ label="&lightning.menu.view.tasks.label;"
+ accesskey="&lightning.menu.view.tasks.accesskey;">
+ <menupopup id="ltnTasksMenuPopup">
+ <observes element="filterBroadcaster"
+ attribute="value"
+ onbroadcast="checkRadioControl(this.parentNode, document.getElementById(this.getAttribute('element')).getAttribute('value'));"/>
+ <menuitem id="ltnTasksViewFilterTasks"
+ type="checkbox"
+ label="&calendar.tasks.view.filtertasks.label;"
+ accesskey="&calendar.tasks.view.filtertasks.accesskey;"
+ command="calendar_toggle_filter_command"/>
+ <menuseparator id="ltnTasksViewSeparator"/>
+ <menuitem id="ltnTasksViewFilterCurrent"
+ name="filtergroup"
+ value="throughcurrent"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.current.label;"
+ accesskey="&calendar.task.filter.current.accesskey;"/>
+ <menuitem id="ltnTasksViewFilterToday"
+ name="filtergroup"
+ value="throughtoday"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.today.label;"
+ accesskey="&calendar.task.filter.today.accesskey;"/>
+ <menuitem id="ltnTasksViewFilterNext7days"
+ name="filtergroup"
+ value="throughsevendays"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.next7days.label;"
+ accesskey="&calendar.task.filter.next7days.accesskey;"/>
+ <menuitem id="ltnTasksViewFilterNotstartedtasks"
+ name="filtergroup"
+ value="notstarted"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.notstarted.label;"
+ accesskey="&calendar.task.filter.notstarted.accesskey;"/>
+ <menuitem id="ltnTasksViewFilterOverdue"
+ name="filtergroup"
+ value="overdue"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.overdue.label;"
+ accesskey="&calendar.task.filter.overdue.accesskey;"/>
+ <menuitem id="ltnTasksViewFilterCompleted"
+ name="filtergroup"
+ type="radio"
+ value="completed"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.completed.label;"
+ accesskey="&calendar.task.filter.completed.accesskey;"/>
+ <menuitem id="ltnTasksViewFilterOpen"
+ name="filtergroup"
+ type="radio"
+ value="open"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.open.label;"
+ accesskey="&calendar.task.filter.open.accesskey;"/>
+ <menuitem id="ltnTasksViewFilterAll"
+ name="filtergroup"
+ value="all"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.all.label;"
+ accesskey="&calendar.task.filter.all.accesskey;"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+
+ <menupopup id="menu_GoPopup">
+ <menuitem id="ltnGoToToday"
+ insertafter="goNextSeparator"
+ label="&goTodayCmd.label;"
+ accesskey="&goTodayCmd.accesskey;"
+ observes="calendar_go_to_today_command"
+ key="calendar-go-to-today-key"/>
+ </menupopup>
+
+ <menupopup id="menu_GoNextPopup">
+ <menuseparator id="ltnGoNextSeparator"/>
+ <!-- Label is set up automatically using the view id. When writing a
+ view extension, overlay this menuitem and add a label-<myviewtype>
+ attribute with the correct label -->
+ <menuitem id="calendar-go-menu-next"
+ label=""
+ label-day="&lightning.toolbar.day.label;"
+ label-week="&lightning.toolbar.week.label;"
+ label-multiweek="&lightning.toolbar.week.label;"
+ label-month="&lightning.toolbar.month.label;"
+ accesskey-day="&lightning.toolbar.day.accesskey;"
+ accesskey-week="&lightning.toolbar.week.accesskey;"
+ accesskey-multiweek="&lightning.toolbar.week.accesskey;"
+ accesskey-month="&lightning.toolbar.month.accesskey;"
+ observes="calendar_view_next_command"/>
+ </menupopup>
+
+ <menupopup id="menu_GoPreviousPopup">
+ <menuseparator id="ltnGoPreviousSeparator"/>
+ <!-- Label is set up automatically using the view id. When writing a
+ view extension, overlay this menuitem and add a label-<myviewtype>
+ attribute with the correct label -->
+ <menuitem id="calendar-go-menu-previous"
+ label=""
+ label-day="&lightning.toolbar.day.label;"
+ label-week="&lightning.toolbar.week.label;"
+ label-multiweek="&lightning.toolbar.week.label;"
+ label-month="&lightning.toolbar.month.label;"
+ accesskey-day="&lightning.toolbar.day.accesskey;"
+ accesskey-week="&lightning.toolbar.week.accesskey;"
+ accesskey-multiweek="&lightning.toolbar.week.accesskey;"
+ accesskey-month="&lightning.toolbar.month.accesskey;"
+ observes="calendar_view_prev_command"/>
+ </menupopup>
+
+ <menubar id="mail-menubar">
+ <menu id="menu_Event_Task"
+ label="&lightning.menu.eventtask.label;"
+ accesskey="&lightning.menu.eventtask.accesskey;"
+ insertafter="messageMenu">
+ <menupopup id="menu_Event_Task_Popup" onpopupshowing="changeMenuForTask(event); setupDeleteMenuitem('ltnDeleteSelectedCalendar')">
+ <menuitem id="ltnNewEvent2"
+ label="&event.new.event;"
+ accesskey="&event.new.event.accesskey;"
+ key="calendar-new-event-key"
+ command="calendar_new_event_command"
+ observes="calendar_new_event_command"/>
+ <menuitem id="ltnNewTask2"
+ label="&event.new.task;"
+ accesskey="&event.new.task.accesskey;"
+ key="calendar-new-todo-key"
+ command="calendar_new_todo_command"
+ observes="calendar_new_todo_command"/>
+ <menuseparator id="before-Calendar-Mode-Section"/>
+ <menuitem id="ltnMenuSwitchToCalendar"
+ type="checkbox"
+ label="&lightning.toolbar.calendar.label;"
+ accesskey="&lightning.toolbar.calendar.accesskey;"
+ command="switch2calendar"
+ key="openLightningKey"
+ autocheck="false">
+ <observes element="modeBroadcaster"
+ attribute="mode"
+ onbroadcast="this.parentNode.setAttribute('checked', '' + document.getElementById('modeBroadcaster').getAttribute('mode') == 'calendar')"/>
+ </menuitem>
+ <menuitem id="ltnMenuSwitchToTask"
+ type="checkbox"
+ label="&lightning.toolbar.task.label;"
+ accesskey="&lightning.toolbar.task.accesskey;"
+ command="switch2task"
+ key="openTasksKey"
+ autocheck="false">
+ <observes element="modeBroadcaster"
+ attribute="mode"
+ onbroadcast="this.parentNode.setAttribute('checked', '' + document.getElementById('modeBroadcaster').getAttribute('mode') == 'task')"/>
+ </menuitem>
+ <menuseparator id="ltnBeforeCalendarSection"/>
+ <!-- Menuitems have different schema just to match sunbird -->
+ <menuitem id="calendar-export-menu"
+ label="&calendar.export.label;"
+ accesskey="&calendar.export.accesskey;"
+ command="calendar_export_command"
+ observes="calendar_export_command"/>
+ <menuitem id="calendar-import-menu"
+ label="&calendar.import.label;"
+ accesskey="&calendar.import.accesskey;"
+ command="calendar_import_command"
+ observes="calendar_import_command"/>
+ <menuitem id="ltnPublishCalendar"
+ label="&calendar.publish.label;"
+ accesskey="&calendar.publish.accesskey;"
+ commmand="calendar_publish_calendar_command"
+ observes="calendar_publish_calendar_command"/>
+ <menuitem id="ltnDeleteSelectedCalendar"
+ labeldelete="&calendar.deletecalendar.label;"
+ labelremove="&calendar.removecalendar.label;"
+ labelunsubscribe="&calendar.unsubscribecalendar.label;"
+ accesskeydelete="&calendar.deletecalendar.accesskey;"
+ accesskeyremove="&calendar.removecalendar.accesskey;"
+ accesskeyunsubscribe="&calendar.unsubscribecalendar.accesskey;"
+ command="calendar_delete_calendar_command"
+ observes="calendar_delete_calendar_command"/>
+ <menuseparator id="ltnBeforeTaskActions"/>
+ <menuitem id="ltnTaskActionsMarkCompletedMenuitem"
+ type="checkbox"
+ label="&calendar.context.markcompleted.label;"
+ accesskey="&calendar.context.markcompleted.accesskey;"
+ command="calendar_toggle_completed_command"
+ observes="calendar_toggle_completed_command"/>
+ <menu id="ltnTaskActionsPriorityMenuitem"
+ label="&calendar.context.priority.label;"
+ accesskey="&calendar.context.priority.accesskey;"
+ command="calendar_general-priority_command"
+ observes="calendar_general-priority_command">
+ <menupopup type="task-priority"/>
+ </menu>
+ <menu id="ltnTaskActionsProgressMenuitem"
+ label="&calendar.context.progress.label;"
+ accesskey="&calendar.context.progress.accesskey;"
+ command="calendar_general-progress_command"
+ observes="calendar_general-progress_command">
+ <menupopup type="task-progress"/>
+ </menu>
+ <menu id="ltnTaskActionsPostponeMenuitem"
+ label="&calendar.context.postpone.label;"
+ accesskey="&calendar.context.postpone.accesskey;"
+ observes="calendar_general-postpone_command">
+ <menupopup id="ltnTaskActionsPostponeMenuPopup">
+ <menuitem id="ltnTaskActionsPostponeMenu-1hour"
+ label="&calendar.context.postpone.1hour.label;"
+ accesskey="&calendar.context.postpone.1hour.accesskey;"
+ observes="calendar_postpone-1hour_command"/>
+ <menuitem id="ltnTaskActionsPostponeMenu-1day"
+ label="&calendar.context.postpone.1day.label;"
+ accesskey="&calendar.context.postpone.1day.accesskey;"
+ observes="calendar_postpone-1day_command"/>
+ <menuitem id="ltnTaskActionsPostponeMenu-1week"
+ label="&calendar.context.postpone.1week.label;"
+ accesskey="&calendar.context.postpone.1week.accesskey;"
+ observes="calendar_postpone-1week_command"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="ltnBeforeUnifinderSection" />
+ <!-- menuitem has different schema just to match sunbird -->
+ <menuitem id="calendar-show-unifinder-menu"
+ type="checkbox"
+ checked="true"
+ label="&showUnifinderCmd.label;"
+ accesskey="&showUnifinderCmd.accesskey;"
+ observes="calendar_show_unifinder_command"
+ command="calendar_show_unifinder_command"/>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <menupopup id="mailContext">
+ <menu id="mailContext-calendar-convert-menu"
+ insertafter="mailContext-moveToFolderAgain"
+ label="&calendar.context.convertmenu.label;"
+ accesskey="&calendar.context.convertmenu.accesskey.mail;">
+ <menupopup id="mailContext-calendar-convert-menupopup">
+ <menuitem id="mailContext-calendar-convert-event-menuitem"
+ label="&calendar.context.convertmenu.event.label;"
+ accesskey="&calendar.context.convertmenu.event.accesskey;"
+ oncommand="calendarExtract.extractFromEmail(true)" />
+ <menuitem id="mailContext-calendar-convert-task-menuitem"
+ label="&calendar.context.convertmenu.task.label;"
+ accesskey="&calendar.context.convertmenu.task.accesskey;"
+ oncommand="calendarExtract.extractFromEmail(false)" />
+ </menupopup>
+ </menu>
+ </menupopup>
+
+ <menupopup id="toolbar-context-menu"
+ onpopupshowing="onToolbarsPopupShowingForTabType(event);"/>
+
+<!-- AppMenu integration -->
+ <menupopup id="appmenu_newMenupopup">
+ <menuitem id="appmenu_ltnNewEvent"
+ label="&lightning.menupopup.new.event.label;"
+ insertbefore="appmenu_newFolder"
+ command="calendar_new_event_command"
+ observes="calendar_new_event_command"/>
+ <menuitem id="appmenu_ltnNewTask"
+ label="&lightning.menupopup.new.task.label;"
+ insertbefore="appmenu_newFolder"
+ command="calendar_new_todo_command"
+ observes="calendar_new_todo_command"/>
+ <menuseparator id="appmenu_afterltnNewTask"
+ insertbefore="appmenu_newFolder"
+ observes="appmenu_newFolder"/>
+ <menuitem id="appmenu_ltnNewCalendar" label="&lightning.menupopup.new.calendar.label;"
+ command="calendar_new_calendar_command"
+ observes="calendar_new_calendar_command"
+ insertafter="appmenu_newAccountMenuItem"/>
+ </menupopup>
+ <splitmenu id="appmenu_customize">
+ <menupopup id="appmenu_customizeMenu"
+ onpopupshowing="onToolbarsPopupShowingForTabType(event, document.getElementById('appmenu_quickFilterBar'));"/>
+ </splitmenu>
+ <menupopup id="appmenu_FilePopup">
+ <menu id="appmenu_Open"
+ mode="calendar"
+ label="&lightning.menupopup.open.label;"
+ accesskey="&lightning.menupopup.open.accesskey;"
+ insertbefore="appmenu_openMessageFileMenuitem">
+ <menupopup id="appmenu_OpenPopup">
+ <menuitem id="appmenu_OpenMessageFileMenuitem"
+ label="&lightning.menupopup.open.message.label;"
+ accesskey="&lightning.menupopup.open.message.accesskey;"
+ oncommand="MsgOpenFromFile();"/>
+ <menuitem id="appmenu_OpenCalendarFileMenuitem"
+ label="&lightning.menupopup.open.calendar.label;"
+ accesskey="&lightning.menupopup.open.calendar.accesskey;"
+ oncommand="openLocalCalendar();"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ <menuitem id="appmenu_openMessageFileMenuitem" hidden="true"/>
+ <menupopup id="appmenu_View_Popup">
+ <menuseparator id="appmenu_ltnViewMenuSeparator"/>
+ <menu id="appmenu_ltnTodayPaneMenu"
+ label="&calendar.context.button.label;">
+ <menupopup id="appmenu_ltnTodayPaneMenuPopup">
+ <menuitem id="appmenu_ltnShowTodayPane-2"
+ label="&todaypane.showTodayPane.label;"
+ type="checkbox"
+ command="calendar_toggle_todaypane_command"/>
+ <menuseparator id="appmenu_ltnSeparatorBeforeDisplayMiniday"/>
+ <menuitem id="appmenu_ltnTodayPaneDisplayMiniday"
+ name="minidisplay"
+ value="miniday"
+ type="radio"
+ oncommand="TodayPane.displayMiniSection('miniday')"
+ observes="ltnTodayPaneDisplayMiniday"
+ label="&todaypane.showMiniday.label;"/>
+ <menuitem id="appmenu_ltnTodayPaneDisplayMinimonth"
+ name="minidisplay"
+ value="minimonth"
+ type="radio"
+ oncommand="TodayPane.displayMiniSection('minimonth')"
+ observes="ltnTodayPaneDisplayMinimonth"
+ label="&todaypane.showMinimonth.label;"/>
+ <menuitem id="appmenu_ltnTodayPaneDisplayNone"
+ name="minidisplay"
+ value="none"
+ type="radio"
+ oncommand="TodayPane.displayMiniSection('none')"
+ observes="ltnTodayPaneDisplayNone"
+ label="&todaypane.showNone.label;"/>
+ </menupopup>
+ </menu>
+ <menu id="appmenu_ltnCalendarMenu"
+ observes="calendar_in_foreground"
+ insertbefore="viewSortMenuSeparator"
+ label="&lightning.menu.view.calendar.label;">
+ <menupopup id="appmenu_ltnCalendarMenuPopup">
+ <menuitem id="appmenu_ltnChangeViewDay"
+ label="&lightning.toolbar.day.label;"
+ type="radio"
+ name="calendarMenuViews"
+ command="calendar_day-view_command"/>
+ <menuitem id="appmenu_ltnChangeViewWeek"
+ label="&lightning.toolbar.week.label;"
+ type="radio"
+ name="calendarMenuViews"
+ command="calendar_week-view_command"/>
+ <menuitem id="appmenu_ltnChangeViewMultiweek"
+ label="&lightning.toolbar.multiweek.label;"
+ type="radio"
+ name="calendarMenuViews"
+ command="calendar_multiweek-view_command"/>
+ <menuitem id="appmenu_ltnChangeViewMonth"
+ label="&lightning.toolbar.month.label;"
+ type="radio"
+ name="calendarMenuViews"
+ command="calendar_month-view_command"/>
+ <menuseparator id="appmenu_ltnBeforeCalendarViewSection"/>
+ <menu id="appmenu_ltnCalendarPaneMenu"
+ label="&lightning.toolbar.calendarmenu.label;">
+ <menupopup id="appmenu_ltnCalendarPanePopup"
+ onpopupshowing="InitViewCalendarPaneMenu()">
+ <menuitem id="appmenu_ltnViewCalendarPane"
+ type="checkbox"
+ label="&lightning.toolbar.calendarpane.label;"
+ command="calendar_toggle_calendarsidebar_command"/>
+ <menuseparator id="appmenu_ltnCalendarPaneMenuSeparator"/>
+ <menuitem id="appmenu_ltnTasksViewMinimonth"
+ type="checkbox"
+ label="&calendar.tasks.view.minimonth.label;"
+ command="calendar_toggle_minimonthpane_command"/>
+ <menuitem id="appmenu_ltnTasksViewCalendarlist"
+ type="checkbox"
+ label="&calendar.tasks.view.calendarlist.label;"
+ command="calendar_toggle_calendarlist_command"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="appmenu_ltnBeforeCurrentViewMenu"/>
+ <menu id="appmenu_ltnCalendarCurrentViewMenu"
+ observes="calendar_mode_calendar"
+ label="&showCurrentView.label;">
+ <menupopup id="appmenu_ltnCalendarCurrentViewMenuPopup">
+ <menuitem type="checkbox"
+ id="appmenu_ltnWorkdaysOnlyMenuitem"
+ label="&calendar.onlyworkday.checkbox.label;"
+ observes="calendar_toggle_workdays_only_command"/>
+ <menuitem type="checkbox"
+ id="appmenu_ltnTasksInViewMenuitem"
+ label="&calendar.displaytodos.checkbox.label;"
+ observes="calendar_toggle_tasks_in_view_command"/>
+ <menuitem type="checkbox"
+ id="appmenu_ltnShowCompletedInViewMenuItem"
+ label="&calendar.completedtasks.checkbox.label;"
+ observes="calendar_toggle_show_completed_in_view_command"/>
+ <menuitem type="checkbox"
+ id="appmenu_ltnViewRotated"
+ label="&calendar.orientation.label;"
+ command="calendar_toggle_orientation_command"
+ observes="calendar_toggle_orientation_command"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ <menu id="appmenu_ltnTasksMenu"
+ observes="calendar_mode_task"
+ insertbefore="viewSortMenuSeparator"
+ label="&lightning.menu.view.tasks.label;">
+ <menupopup id="appmenu_ltnTasksMenuPopup">
+ <observes element="filterBroadcaster"
+ attribute="value"
+ onbroadcast="checkRadioControl(this.parentNode, document.getElementById(this.getAttribute('element')).getAttribute('value'));"/>
+ <menuitem id="appmenu_ltnTasksViewFilterTasks"
+ type="checkbox"
+ label="&calendar.tasks.view.filtertasks.label;"
+ command="calendar_toggle_filter_command"/>
+ <menuseparator id="appmenu_ltnTasksViewSeparator"/>
+ <menuitem id="appmenu_ltnTasksViewFilterCurrent"
+ name="filtergroup"
+ value="throughcurrent"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.current.label;"/>
+ <menuitem id="appmenu_ltnTasksViewFilterToday"
+ name="filtergroup"
+ value="throughtoday"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.today.label;"/>
+ <menuitem id="appmenu_ltnTasksViewFilterNext7days"
+ name="filtergroup"
+ value="throughsevendays"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.next7days.label;"/>
+ <menuitem id="appmenu_ltnTasksViewFilterNotstartedtasks"
+ name="filtergroup"
+ value="notstarted"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.notstarted.label;"/>
+ <menuitem id="appmenu_ltnTasksViewFilterOverdue"
+ name="filtergroup"
+ value="overdue"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.overdue.label;"/>
+ <menuitem id="appmenu_ltnTasksViewFilterCompleted"
+ name="filtergroup"
+ type="radio"
+ value="completed"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.completed.label;"/>
+ <menuitem id="appmenu_ltnTasksViewFilterOpen"
+ name="filtergroup"
+ type="radio"
+ value="open"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.open.label;"/>
+ <menuitem id="appmenu_ltnTasksViewFilterAll"
+ name="filtergroup"
+ value="all"
+ type="radio"
+ command="calendar_task_filter_command"
+ label="&calendar.task.filter.all.label;"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+
+ <menupopup id="appmenu_GoPopup">
+ <menuitem id="appmenu_ltnGoToToday"
+ insertafter="appmenu_goNextSeparator"
+ label="&goTodayCmd.label;"
+ observes="calendar_go_to_today_command"/>
+ </menupopup>
+
+ <menupopup id="appmenu_GoNextPopup">
+ <menuseparator id="appmenu_ltnGoNextSeparator"/>
+ <!-- Label is set up automatically using the view id. When writing a
+ view extension, overlay this menuitem and add a label-<myviewtype>
+ attribute with the correct label -->
+ <menuitem id="appmenu_calendar-go-menu-next"
+ label=""
+ label-day="&lightning.toolbar.day.label;"
+ label-week="&lightning.toolbar.week.label;"
+ label-multiweek="&lightning.toolbar.week.label;"
+ label-month="&lightning.toolbar.month.label;"
+ observes="calendar_view_next_command"/>
+ </menupopup>
+ <menupopup id="appmenu_GoPreviousPopup">
+ <menuseparator id="appmenu_ltnGoPreviousSeparator"/>
+ <!-- Label is set up automatically using the view id. When writing a
+ view extension, overlay this menuitem and add a label-<myviewtype>
+ attribute with the correct label -->
+ <menuitem id="appmenu_calendar-go-menu-previous"
+ label=""
+ label-day="&lightning.toolbar.day.label;"
+ label-week="&lightning.toolbar.week.label;"
+ label-multiweek="&lightning.toolbar.week.label;"
+ label-month="&lightning.toolbar.month.label;"
+ observes="calendar_view_prev_command"/>
+ </menupopup>
+
+ <vbox id="appmenuSecondaryPane">
+ <menu id="appmenu_Event_Task"
+ label="&lightning.menu.eventtask.label;"
+ insertafter="appmenu_messageMenu">
+ <menupopup id="appmenu_Event_Task_Popup" onpopupshowing="changeMenuForTask(event); setupDeleteMenuitem('appmenu_ltnDeleteSelectedCalendar')">
+ <menuitem id="appmenu_ltnMenuSwitchToCalendar"
+ type="checkbox"
+ label="&lightning.toolbar.calendar.label;"
+ command="switch2calendar"
+ autocheck="false">
+ <observes element="modeBroadcaster"
+ attribute="mode"
+ onbroadcast="this.parentNode.setAttribute('checked', '' + document.getElementById('modeBroadcaster').getAttribute('mode') == 'calendar')"/>
+ </menuitem>
+ <menuitem id="appmenu_ltnMenuSwitchToTask"
+ type="checkbox"
+ label="&lightning.toolbar.task.label;"
+ command="switch2task"
+ autocheck="false">
+ <observes element="modeBroadcaster"
+ attribute="mode"
+ onbroadcast="this.parentNode.setAttribute('checked', '' + document.getElementById('modeBroadcaster').getAttribute('mode') == 'task')"/>
+ </menuitem>
+ <menuseparator id="appmenu_ltnBeforeCalendarSection"/>
+ <!-- Menuitems have different schema just to match sunbird -->
+ <menuitem id="appmenu_calendar-export-menu"
+ label="&calendar.export.label;"
+ command="calendar_export_command"
+ observes="calendar_export_command"/>
+ <menuitem id="appmenu_calendar-import-menu"
+ label="&calendar.import.label;"
+ command="calendar_import_command"
+ observes="calendar_import_command"/>
+ <menuitem id="appmenu_ltnPublishCalendar"
+ label="&calendar.publish.label;"
+ commmand="calendar_publish_calendar_command"
+ observes="calendar_publish_calendar_command"/>
+ <menuitem id="appmenu_ltnDeleteSelectedCalendar"
+ labeldelete="&calendar.deletecalendar.label;"
+ labelremove="&calendar.removecalendar.label;"
+ labelunsubscribe="&calendar.unsubscribecalendar.label;"
+ accesskeydelete="&calendar.deletecalendar.accesskey;"
+ accesskeyremove="&calendar.removecalendar.accesskey;"
+ accesskeyunsubscribe="&calendar.unsubscribecalendar.accesskey;"
+ command="calendar_delete_calendar_command"
+ observes="calendar_delete_calendar_command"/>
+ <menuseparator id="ltnBeforeTaskActions"/>
+ <menuitem id="appmenu_ltnTaskActionsMarkCompletedMenuitem"
+ type="checkbox"
+ label="&calendar.context.markcompleted.label;"
+ command="calendar_toggle_completed_command"
+ observes="calendar_toggle_completed_command"/>
+ <menu id="appmenu_ltnTaskActionsPriorityMenuitem"
+ label="&calendar.context.priority.label;"
+ command="calendar_general-priority_command"
+ observes="calendar_general-priority_command">
+ <menupopup type="task-priority"/>
+ </menu>
+ <menu id="appmenu_ltnTaskActionsProgressMenuitem"
+ label="&calendar.context.progress.label;"
+ command="calendar_general-progress_command"
+ observes="calendar_general-progress_command">
+ <menupopup type="task-progress"/>
+ </menu>
+ <menu id="appmenu_ltnTaskActionsPostponeMenuitem"
+ label="&calendar.context.postpone.label;"
+ observes="calendar_general-postpone_command">
+ <menupopup id="appmenu_ltnTaskActionsPostponeMenuPopup">
+ <menuitem id="ltnTaskActionsPostponeMenu-1hour"
+ label="&calendar.context.postpone.1hour.label;"
+ observes="calendar_postpone-1hour_command"/>
+ <menuitem id="appmenu_ltnTaskActionsPostponeMenu-1day"
+ label="&calendar.context.postpone.1day.label;"
+ observes="calendar_postpone-1day_command"/>
+ <menuitem id="appmenu_ltnTaskActionsPostponeMenu-1week"
+ label="&calendar.context.postpone.1week.label;"
+ observes="calendar_postpone-1week_command"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="appmenu_ltnBeforeUnifinderSection" />
+ <!-- menuitem has different schema just to match sunbird -->
+ <menuitem id="appmenu_calendar-show-unifinder-menu"
+ type="checkbox"
+ checked="true"
+ label="&showUnifinderCmd.label;"
+ observes="calendar_show_unifinder_command"
+ command="calendar_show_unifinder_command"/>
+ <menuseparator id="appmenu_ltnBeforeCalendarProperties" />
+ <menuitem id="appmenu_ltnCalendarProperties"
+ insertafter="menu_properties"
+ label="&calendar.properties.label;"
+ command="calendar_edit_calendar_command"
+ observes="calendar_edit_calendar_command"/>
+ </menupopup>
+ </menu>
+ </vbox>
+</overlay>
diff --git a/calendar/lightning/content/lightning-migration.xul b/calendar/lightning/content/lightning-migration.xul
new file mode 100644
index 000000000..201c39661
--- /dev/null
+++ b/calendar/lightning/content/lightning-migration.xul
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- The old calendar extension, if it is installed in the same profile as
+ - Lightning, will break Lightning because it ships several files that
+ - have the same chrome address as files that Lightning ships. This file
+ - exists so we can check for whether that extension is installed and nuke it
+ - in that case. Note that this check *cannot* be done in any file that may
+ - die as a result of the conflict (including messanger-overlay-sidebar.js).
+ - Nor can it depend on files which may conflict.
+ -->
+
+<!-- DTD File with all strings specific to the file -->
+<!DOCTYPE overlay
+[
+]>
+
+<overlay id="ltnMigrationOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://calendar/content/calendar-migration-dialog.js"/>
+ <script type="application/javascript"><![CDATA[
+ function checkOld() {
+ window.removeEventListener("load", checkOld, false);
+ var calMgr = Components.classes["@mozilla.org/calendar/manager;1"]
+ .getService(Components.interfaces.calICalendarManager);
+ var cals = calMgr.getCalendars({});
+ if (!cals.length) {
+ // There are no calendars, so we are running for the first time
+ gDataMigrator.checkAndMigrate();
+ }
+ }
+ window.addEventListener("load", checkOld, false);
+ ]]></script>
+
+ <deck id="calendarDisplayDeck"/>
+
+</overlay>
diff --git a/calendar/lightning/content/lightning-toolbar.xul b/calendar/lightning/content/lightning-toolbar.xul
new file mode 100644
index 000000000..3dda61880
--- /dev/null
+++ b/calendar/lightning/content/lightning-toolbar.xul
@@ -0,0 +1,223 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd"> %messengerDTD;
+ <!ENTITY % mailOverlayDTD SYSTEM "chrome://messenger/locale/mailOverlay.dtd"> %mailOverlayDTD;
+ <!ENTITY % menuOverlayDTD SYSTEM "chrome://calendar/locale/menuOverlay.dtd" > %menuOverlayDTD;
+ <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd"> %lightningDTD;
+ <!ENTITY % calendarDTD SYSTEM "chrome://calendar/locale/calendar.dtd" > %calendarDTD;
+ <!ENTITY % toolbarDTD SYSTEM "chrome://lightning/locale/lightning-toolbar.dtd" > %toolbarDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > %brandDTD;
+]>
+
+<?xml-stylesheet href="chrome://lightning/skin/lightning-toolbar.css" type="text/css"?>
+
+<overlay id="ltnToolbarOverlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <popupset id="calendar-popupset">
+ <menupopup id="calendar-toolbar-context"
+ onpopupshowing="onViewToolbarsPopupShowing(event, ['navigation-toolbox', 'calendar-toolbox']);">
+ <menuseparator id="customizeCalendarToolbarMenuSeparator"/>
+ <menuitem id="CustomizeCalendarToolbar"
+ label="&calendar.menu.customize.label;"
+ accesskey="&calendar.menu.customize.accesskey;"
+ oncommand="CustomizeMailToolbar('calendar-toolbox', 'CustomizeCalendarToolbar')"/>
+ </menupopup>
+ <menupopup id="task-toolbar-context"
+ onpopupshowing="onViewToolbarsPopupShowing(event, ['navigation-toolbox', 'task-toolbox']);">
+ <menuseparator id="customizeTaskToolbarMenuSeparator"/>
+ <menuitem id="CustomizeTaskToolbar"
+ label="&calendar.menu.customize.label;"
+ accesskey="&calendar.menu.customize.accesskey;"
+ oncommand="CustomizeMailToolbar('task-toolbox', 'CustomizeTaskToolbar')"/>
+ </menupopup>
+ </popupset>
+
+ <!-- The popup id here must match the popup id in the SeaMonkey
+ New Message button. See Bug 506461 -->
+ <toolbarbutton id="button-newmsg"
+ type="menu-button">
+ <menupopup id="button-newMsgPopup">
+ <menuitem id="newMsgButton-mail-menuitem"
+ label="&newMessageCmd.label;"
+ class="menuitem-iconic"
+ oncommand="event.stopPropagation(); MsgNewMessage(event)"/>
+ <menuitem id="newMsgButton-calendar-menuitem"
+ label="&lightning.toolbar.newevent.label;"
+ class="menuitem-iconic"
+ command="calendar_new_event_command"
+ observes="calendar_new_event_command"/>
+ <menuitem id="newMsgButton-task-menuitem"
+ label="&lightning.toolbar.newtask.label;"
+ class="menuitem-iconic"
+ command="calendar_new_todo_command"
+ observes="calendar_new_todo_command"/>
+ </menupopup>
+ </toolbarbutton>
+
+ <toolbarpalette id="MailToolbarPalette">
+ <toolbarbutton id="lightning-button-calendar"
+ class="toolbarbutton-1"
+ label="&lightning.toolbar.calendar.label;"
+ tooltiptext="&lightning.toolbar.calendar.tooltip;"
+ command="new_calendar_tab"/>
+ <toolbarbutton id="lightning-button-tasks"
+ class="toolbarbutton-1"
+ label="&lightning.toolbar.task.label;"
+ tooltiptext="&lightning.toolbar.task.tooltip;"
+ command="new_task_tab"/>
+ <toolbarbutton id="extractEventButton"
+ class="toolbarbutton-1"
+ type="menu-button"
+ label="&calendar.extract.event.button;"
+ tooltiptext="&calendar.extract.event.button.tooltip;"
+ oncommand="calendarExtract.extractFromEmail(true);">
+ <menupopup id="extractEventLocaleList"
+ oncommand="calendarExtract.extractWithLocale(event, true);"
+ onpopupshowing="calendarExtract.onShowLocaleMenu(event.target);"/>
+ </toolbarbutton>
+ <toolbarbutton id="extractTaskButton"
+ class="toolbarbutton-1"
+ type="menu-button"
+ label="&calendar.extract.task.button;"
+ tooltiptext="&calendar.extract.task.button.tooltip;"
+ oncommand="calendarExtract.extractFromEmail(false);">
+ <menupopup id="extractTaskLocaleList"
+ oncommand="calendarExtract.extractWithLocale(event, false);"
+ onpopupshowing="calendarExtract.onShowLocaleMenu(event.target);"/>
+ </toolbarbutton>
+ </toolbarpalette>
+
+ <toolbarpalette id="header-view-toolbar-palette">
+ <toolbarbutton id="hdrExtractEventButton"
+ class="toolbarbutton-1 msgHeaderView-button"
+ type="menu-button"
+ label="&calendar.extract.event.button;"
+ tooltiptext="&calendar.extract.event.button.tooltip;"
+ oncommand="calendarExtract.extractFromEmail(true)">
+ <menupopup id="hdrExtractEventLocaleList"
+ oncommand="calendarExtract.extractWithLocale(event, true);"
+ onpopupshowing="calendarExtract.onShowLocaleMenu(event.target);"/>
+ </toolbarbutton>
+ <toolbarbutton id="hdrExtractTaskButton"
+ class="toolbarbutton-1 msgHeaderView-button"
+ type="menu-button"
+ label="&calendar.extract.task.button;"
+ tooltiptext="&calendar.extract.task.button.tooltip;"
+ oncommand="calendarExtract.extractFromEmail(false)">
+ <menupopup id="hdrExtractTaskLocaleList"
+ oncommand="calendarExtract.extractWithLocale(event, false);"
+ onpopupshowing="calendarExtract.onShowLocaleMenu(event.target);"/>
+ </toolbarbutton>
+ </toolbarpalette>
+
+ <toolbox id="calendar-toolbox"
+ class="mail-toolbox"
+ mode="full"
+ defaultmode="full"
+ labelalign="end"
+ defaultlabelalign="end">
+ <toolbarpalette id="CalendarToolbarPalette">
+ <toolbarbutton id="calendar-synchronize-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.sync.label;"
+ tooltiptext="&lightning.toolbar.sync.tooltip;"
+ observes="calendar_reload_remote_calendars"/>
+ <toolbarbutton id="calendar-newevent-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.newevent.label;"
+ tooltiptext="&lightning.toolbar.newevent.tooltip;"
+ observes="calendar_new_event_command"/>
+ <toolbarbutton id="calendar-newtask-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.newtask.label;"
+ tooltiptext="&lightning.toolbar.newtask.tooltip;"
+ observes="calendar_new_todo_command"/>
+ <toolbarbutton id="calendar-goto-today-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.gototoday.label;"
+ tooltiptext="&lightning.toolbar.gototoday.tooltip;"
+ observes="calendar_go_to_today_command"/>
+ <toolbarbutton id="calendar-edit-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.edit.label;"
+ tooltiptext="&lightning.toolbar.edit.tooltip;"
+ observes="calendar_modify_focused_item_command"/>
+ <toolbarbutton id="calendar-delete-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.delete.label;"
+ tooltiptext="&lightning.toolbar.delete.tooltip;"
+ observes="calendar_delete_focused_item_command"/>
+ <toolbarbutton id="calendar-print-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.print.label;"
+ tooltiptext="&lightning.toolbar.print.tooltip;"
+ observes="cmd_print"/>
+ <toolbarbutton id="calendar-unifinder-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&showUnifinderCmd.label;"
+ tooltiptext="&showUnifinderCmd.tooltip;"
+ observes="calendar_show_unifinder_command"/>
+ </toolbarpalette>
+
+ <toolbar id="calendar-toolbar2" class="inline-toolbar chromeclass-toolbar"
+ toolbarname="&lightning.toolbar.calendar.name;"
+ accesskey="&lightning.toolbar.calendar.name.accesskey;"
+ fullscreentoolbar="true" mode="full"
+ customizable="true"
+ context="calendar-toolbar-context"
+ defaultset="calendar-synchronize-button,calendar-newevent-button,calendar-newtask-button,calendar-edit-button,calendar-delete-button,spring"/>
+ <toolbarset id="calendarToolbars" context="calendar-toolbar-context"/>
+ </toolbox>
+
+ <toolbox id="task-toolbox"
+ class="mail-toolbox"
+ mode="full"
+ defaultmode="full"
+ labelalign="end"
+ defaultlabelalign="end">
+ <toolbarpalette id="TaskToolbarPalette">
+ <toolbarbutton id="task-synchronize-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.sync.label;"
+ tooltiptext="&lightning.toolbar.sync.tooltip;"
+ observes="calendar_reload_remote_calendars"/>
+ <toolbarbutton id="task-newevent-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.newevent.label;"
+ tooltiptext="&lightning.toolbar.newevent.tooltip;"
+ observes="calendar_new_event_command"/>
+ <toolbarbutton id="task-newtask-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.newtask.label;"
+ tooltiptext="&lightning.toolbar.newtask.tooltip;"
+ observes="calendar_new_todo_command"/>
+ <toolbarbutton id="task-edit-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.edit.label;"
+ tooltiptext="&lightning.toolbar.edit.tooltip;"
+ observes="calendar_modify_focused_item_command"/>
+ <toolbarbutton id="task-delete-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.delete.label;"
+ tooltiptext="&lightning.toolbar.delete.tooltip;"
+ observes="calendar_delete_focused_item_command"/>
+ <toolbarbutton id="task-print-button"
+ class="toolbarbutton-1 calbar-toolbarbutton-1"
+ label="&lightning.toolbar.print.label;"
+ tooltiptext="&lightning.toolbar.print.tooltip;"
+ observes="cmd_print"/>
+ </toolbarpalette>
+
+ <toolbar id="task-toolbar2" class="inline-toolbar chromeclass-toolbar"
+ toolbarname="&lightning.toolbar.task.name;"
+ accesskey="&lightning.toolbar.task.name.accesskey;"
+ fullscreentoolbar="true" mode="full"
+ customizable="true"
+ context="task-toolbar-context"
+ defaultset="task-synchronize-button,task-newevent-button,task-newtask-button,task-edit-button,task-delete-button,spring"/>
+ <toolbarset id="taskToolbars" context="task-toolbar-context"/>
+ </toolbox>
+</overlay>
diff --git a/calendar/lightning/content/lightning-utils.js b/calendar/lightning/content/lightning-utils.js
new file mode 100644
index 000000000..61383a3d3
--- /dev/null
+++ b/calendar/lightning/content/lightning-utils.js
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported ltnInitMailIdentitiesRow, ltnSaveMailIdentitySelection */
+
+Components.utils.import("resource:///modules/iteratorUtils.jsm");
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+/**
+ * Gets the value of a string in a .properties file from the lightning bundle
+ *
+ * @param aBundleName the name of the properties file. It is assumed that the
+ * file lives in chrome://lightning/locale/
+ * @param aStringName the name of the string within the properties file
+ * @param aParams optional array of parameters to format the string
+ */
+function ltnGetString(aBundleName, aStringName, aParams) {
+ return cal.calGetString(aBundleName, aStringName, aParams, "lightning");
+}
+
+// shared by lightning-calendar-properties.js and lightning-calendar-creation.js:
+function ltnInitMailIdentitiesRow() {
+ if (!gCalendar) {
+ collapseElement("calendar-email-identity-row");
+ }
+
+ let imipIdentityDisabled = gCalendar.getProperty("imip.identity.disabled");
+ setElementValue("calendar-email-identity-row",
+ imipIdentityDisabled && "true",
+ "collapsed");
+
+ if (imipIdentityDisabled) {
+ // If the imip identity is disabled, we don't have to set up the
+ // menulist.
+ return;
+ }
+
+ // If there is no transport but also no organizer id, then the
+ // provider has not statically configured an organizer id. This is
+ // basically what happens when "None" is selected.
+ let menuPopup = document.getElementById("email-identity-menupopup");
+
+ // Remove all children from the email list to avoid duplicates if the list
+ // has already been populated during a previous step in the calendar
+ // creation wizard.
+ while (menuPopup.hasChildNodes()) {
+ menuPopup.lastChild.remove();
+ }
+
+ addMenuItem(menuPopup, ltnGetString("lightning", "imipNoIdentity"), "none");
+ let identities;
+ if (gCalendar && gCalendar.aclEntry && gCalendar.aclEntry.hasAccessControl) {
+ identities = gCalendar.aclEntry.getOwnerIdentities({});
+ } else {
+ identities = MailServices.accounts.allIdentities;
+ }
+ for (let identity in fixIterator(identities, Components.interfaces.nsIMsgIdentity)) {
+ addMenuItem(menuPopup, identity.identityName, identity.key);
+ }
+ try {
+ let sel = gCalendar.getProperty("imip.identity");
+ if (sel) {
+ sel = sel.QueryInterface(Components.interfaces.nsIMsgIdentity);
+ }
+ menuListSelectItem("email-identity-menulist", sel ? sel.key : "none");
+ } catch (exc) {
+ // Don't select anything if the message identity can't be found
+ }
+}
+
+function ltnSaveMailIdentitySelection() {
+ if (!gCalendar) {
+ return;
+ }
+ let sel = "none";
+ let imipIdentityDisabled = gCalendar.getProperty("imip.identity.disabled");
+ let selItem = document.getElementById("email-identity-menulist").selectedItem;
+ if (!imipIdentityDisabled && selItem) {
+ sel = selItem.getAttribute("value");
+ }
+ // no imip.identity.key will default to the default account/identity, whereas
+ // an empty key indicates no imip; that identity will not be found
+ gCalendar.setProperty("imip.identity.key", sel == "none" ? "" : sel);
+}
diff --git a/calendar/lightning/content/lightning-widgets.css b/calendar/lightning/content/lightning-widgets.css
new file mode 100644
index 000000000..f641b24c2
--- /dev/null
+++ b/calendar/lightning/content/lightning-widgets.css
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+lightning-notification-bar {
+ -moz-binding: url(chrome://lightning/content/lightning-widgets.xml#lightning-notification-bar);
+}
+
+#calendar-task-tree-detail > calendar-task-tree {
+ -moz-binding: url(chrome://calendar/content/calendar-task-tree.xml#calendar-task-tree-todaypane);
+}
diff --git a/calendar/lightning/content/lightning-widgets.xml b/calendar/lightning/content/lightning-widgets.xml
new file mode 100644
index 000000000..9c3070b16
--- /dev/null
+++ b/calendar/lightning/content/lightning-widgets.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings id="lightning-widgets"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <binding id="lightning-notification-bar" extends="xul:hbox">
+ <resources>
+ <stylesheet src="chrome://lightning/skin/lightning-widgets.css"/>
+ </resources>
+ <content pack="center" align="center">
+ <xul:image anonid="notification-image"/>
+ <xul:description anonid="notification-description"
+ class="msgNotificationBarText"
+ flex="1"
+ xbl:inherits="xbl:text=label"/>
+ <xul:box anonid="notification-children">
+ <children/>
+ </xul:box>
+ </content>
+ </binding>
+</bindings>
diff --git a/calendar/lightning/content/lightning.js b/calendar/lightning/content/lightning.js
new file mode 100644
index 000000000..2cc01f311
--- /dev/null
+++ b/calendar/lightning/content/lightning.js
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file contains all of the default preference values for Lightning
+
+// Turns on basic calendar logging.
+pref("calendar.debug.log", false);
+// Turns on verbose calendar logging.
+pref("calendar.debug.log.verbose", false);
+
+// addon description
+pref("extensions.{e2fda1a4-762b-4020-b5ad-a41df1933103}.description",
+ "chrome://lightning/locale/lightning.properties");
+pref("extensions.{e2fda1a4-762b-4020-b5ad-a41df1933103}.name",
+ "chrome://lightning/locale/lightning.properties");
+pref("extensions.{e2fda1a4-762b-4020-b5ad-a41df1933103}.creator",
+ "chrome://lightning/locale/lightning.properties");
+
+// general settings
+pref("calendar.date.format", 0);
+pref("calendar.event.defaultlength", 60);
+pref("calendar.task.defaultstart", "none");
+pref("calendar.task.defaultstartoffset", 0);
+pref("calendar.task.defaultstartoffsetunits", "minutes");
+pref("calendar.task.defaultdue", "none");
+pref("calendar.task.defaultdueoffset", 60);
+pref("calendar.task.defaultdueoffsetunits", "minutes");
+
+// default transparency (free-busy status) of standard and all-day events
+pref("calendar.events.defaultTransparency.allday.transparent", true);
+pref("calendar.events.defaultTransparency.standard.transparent", false);
+
+// number of days in "Soon" section
+pref("calendar.agendaListbox.soondays", 5);
+
+// alarm settings
+pref("calendar.alarms.show", true);
+pref("calendar.alarms.showmissed", true);
+pref("calendar.alarms.playsound", true);
+pref("calendar.alarms.soundURL", "chrome://calendar/content/sound.wav");
+pref("calendar.alarms.defaultsnoozelength", 5);
+pref("calendar.alarms.indicator.show", true);
+pref("calendar.alarms.indicator.totaltime", 3600);
+
+// default alarm settings for new event
+pref("calendar.alarms.onforevents", 0);
+pref("calendar.alarms.eventalarmlen", 15);
+pref("calendar.alarms.eventalarmunit", "minutes");
+
+// default alarm settings for new task
+pref("calendar.alarms.onfortodos", 0);
+pref("calendar.alarms.todoalarmlen", 15);
+pref("calendar.alarms.todoalarmunit", "minutes");
+
+// open invitations autorefresh settings
+pref("calendar.invitations.autorefresh.enabled", true);
+pref("calendar.invitations.autorefresh.timeout", 3);
+
+// iTIP compatibility send mode
+// 0 -- Outlook 2003 and following with text/plain and application/ics (default)
+// 1 -- all Outlook, but no text/plain nor application/ics
+// We may extend the compat mode if necessary.
+pref("calendar.itip.compatSendMode", 0);
+
+// whether "notify" is checked by default when creating new events/todos with attendees
+pref("calendar.itip.notify", true);
+
+// whether the organizer propagates replies of attendees to all attendees
+pref("calendar.itip.notify-replies", false);
+
+// whether email invitation updates are send out to all attendees if (only) adding a new attendee
+pref("calendar.itip.updateInvitationForNewAttendeesOnly", false);
+
+//whether changes in email invitation updates should be displayed
+pref("calendar.itip.displayInvitationChanges", true);
+
+//whether for delegated invitations a delegatee's replies will be send also to delegator(s)
+pref("calendar.itip.notifyDelegatorOnReply", true);
+
+// whether to prefix the subject field for email invitation invites or updates.
+pref("calendar.itip.useInvitationSubjectPrefixes", true);
+
+// whether CalDAV (experimental) scheduling is enabled or not.
+pref("calendar.caldav.sched.enabled", false);
+
+// 0=Sunday, 1=Monday, 2=Tuesday, etc. One day we might want to move this to
+// a locale specific file.
+pref("calendar.week.start", 0);
+pref("calendar.weeks.inview", 4);
+pref("calendar.previousweeks.inview", 0);
+
+// Show week number in minimonth and multiweek/month views
+pref("calendar.view-minimonth.showWeekNumber", true);
+
+// Default days off
+pref("calendar.week.d0sundaysoff", true);
+pref("calendar.week.d1mondaysoff", false);
+pref("calendar.week.d2tuesdaysoff", false);
+pref("calendar.week.d3wednesdaysoff", false);
+pref("calendar.week.d4thursdaysoff", false);
+pref("calendar.week.d5fridaysoff", false);
+pref("calendar.week.d6saturdaysoff", true);
+
+// start and end work hour for day and week views
+pref("calendar.view.daystarthour", 8);
+pref("calendar.view.dayendhour", 17);
+
+// number of visible hours for day and week views
+pref("calendar.view.visiblehours", 9);
+
+// time indicator update interval in minutes (0 = no indicator)
+pref("calendar.view.timeIndicatorInterval", 15);
+
+// If true, mouse scrolling via shift+wheel will be enabled
+pref("calendar.view.mousescroll", true);
+
+// Do not set this! If it's not there, then we guess the system timezone
+//pref("calendar.timezone.local", "");
+
+// Recent timezone list
+pref("calendar.timezone.recent", "[]");
+
+// categories settings
+// XXX One day we might want to move this to a locale specific file
+// and include a list of locale specific default categories
+pref("calendar.categories.names", "");
+
+// Make sure mouse wheel shift and no key actions to scroll lines.
+pref("mousewheel.withnokey.action", 0);
+pref("mousewheel.withshiftkey.action", 0);
+
+// Disable use of worker threads. Restart needed.
+pref("calendar.threading.disabled", false);
+
+// The maximum time in microseconds that a cal.forEach event can take (soft limit).
+pref("calendar.threading.latency ", 250);
+
+// Enable support for multiple realms on one server with the payoff that you
+// will get multiple password dialogs (one for each calendar)
+pref("calendar.network.multirealm", false);
+
+// Set up user agent
+#expand pref("calendar.useragent.extra", "Lightning/__LIGHTNING_VERSION__");
+
+// Disable use of system colors in minimonth and calendar views
+pref("calendar.view.useSystemColors", false);
+
+// Disable hiding the label on todayPane button
+pref("calendar.view.showTodayPaneStatusLabel", true);
+
+// Maximum number of iterations allowed when searching for the next matching
+// occurrence of a repeating item in calFilter
+pref("calendar.filter.maxiterations", 50);
+
+// Edit events and tasks in a tab rather than a window.
+pref("calendar.item.editInTab", false);
+
+// Edit events and tasks in the new (HTML-based) UI for tabs and windows
+pref("calendar.item.useNewItemUI", false);
+
+// Backend to use. false: libical, true: ical.js
+#ifdef NIGHTLY_BUILD
+pref("calendar.icaljs", true);
+#else
+pref("calendar.icaljs", false);
+#endif
+
+// Calendar integration notification
+pref("calendar.integration.notify", true);
diff --git a/calendar/lightning/content/messenger-overlay-accountCentral.xul b/calendar/lightning/content/messenger-overlay-accountCentral.xul
new file mode 100644
index 000000000..2c82e7677
--- /dev/null
+++ b/calendar/lightning/content/messenger-overlay-accountCentral.xul
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd">
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %lightningDTD;
+ %globalDTD;
+]>
+
+<?xml-stylesheet href="chrome://lightning/skin/accountCentral.css" type="text/css"?>
+
+<overlay id="calendar-list-overlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <rows id="acctCentralRows">
+ <spacer id="lightning-newCalendar-separator"
+ flex="1"
+ insertbefore="AccountsSection.spacer"/>
+ <row id="lightning-newCalendar-row"
+ class="acctCentralRow"
+ insertbefore="AccountsSection.spacer">
+ <hbox>
+ <label class="acctCentralText acctCentralLinkText"
+ value="&lightning.acctCentral.newCalendar.label;"
+ onclick="window.parent.openCalendarWizard();"/>
+ </hbox>
+ </row>
+ </rows>
+</overlay>
diff --git a/calendar/lightning/content/messenger-overlay-messageWindow.xul b/calendar/lightning/content/messenger-overlay-messageWindow.xul
new file mode 100644
index 000000000..43fd078e3
--- /dev/null
+++ b/calendar/lightning/content/messenger-overlay-messageWindow.xul
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<overlay id="messsenger-overlay-messageWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script type="application/javascript" src="chrome://calendar/content/calendar-statusbar.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-item-editing.js"/>
+</overlay>
diff --git a/calendar/lightning/content/messenger-overlay-preferences.js b/calendar/lightning/content/messenger-overlay-preferences.js
new file mode 100644
index 000000000..8bd34802d
--- /dev/null
+++ b/calendar/lightning/content/messenger-overlay-preferences.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported gLightningPane */
+
+var gLightningPane = {
+ mInitialized: false,
+
+ init: function() {
+ let preference = document.getElementById("calendar.preferences.lightning.selectedTabIndex");
+ if (preference.value) {
+ let ltnPrefs = document.getElementById("calPreferencesTabbox");
+ ltnPrefs.selectedIndex = preference.value;
+ }
+ this.mInitialized = true;
+ },
+
+ tabSelectionChanged: function() {
+ if (!this.mInitialized) {
+ return;
+ }
+ let ltnPrefs = document.getElementById("calPreferencesTabbox");
+ let preference = document.getElementById("calendar.preferences.lightning.selectedTabIndex");
+ preference.valueFromPreferences = ltnPrefs.selectedIndex;
+ }
+};
diff --git a/calendar/lightning/content/messenger-overlay-preferences.xul b/calendar/lightning/content/messenger-overlay-preferences.xul
new file mode 100644
index 000000000..b5759aa70
--- /dev/null
+++ b/calendar/lightning/content/messenger-overlay-preferences.xul
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay [
+ <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd">
+ <!ENTITY % preferencesDTD SYSTEM "chrome://calendar/locale/preferences/preferences.dtd">
+ %lightningDTD;
+ %preferencesDTD;
+]>
+
+<?xml-stylesheet href="chrome://lightning/skin/lightning.css"?>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <prefwindow id="MailPreferences">
+ <prefpane id="paneLightning"
+ insertbefore="paneAdvanced"
+ label="&lightning.preferencesLabel;"
+ onpaneload="gCalendarGeneralPane.init(); gAlarmsPane.init();
+ gCategoriesPane.init(); gViewsPane.init();
+ gLightningPane.init();">
+ <preferences>
+ <preference id="calendar.preferences.lightning.selectedTabIndex"
+ name="calendar.preferences.lightning.selectedTabIndex"
+ type="int"/>
+ </preferences>
+ <tabbox id="calPreferencesTabbox"
+ flex="1"
+ onselect="gLightningPane.tabSelectionChanged();">
+ <tabs>
+ <tab id="calPreferencesTabGeneral"
+ label="&paneGeneral.title;"/>
+ <tab id="calPreferencesTabAlarms"
+ label="&paneAlarms.title;"/>
+ <tab id="calPreferencesTabCategories"
+ label="&paneCategories.title;"/>
+ <tab id="calPreferencesTabViews"
+ label="&paneViews.title;"/>
+ </tabs>
+ <tabpanels flex="1">
+ <tabpanel orient="vertical">
+ <vbox id="calPreferencesBoxGeneral"/>
+ </tabpanel>
+ <tabpanel orient="vertical">
+ <vbox id="calPreferencesBoxAlarms"/>
+ </tabpanel>
+ <tabpanel orient="vertical">
+ <vbox id="calPreferencesBoxCategories"/>
+ </tabpanel>
+ <tabpanel orient="vertical">
+ <vbox id="calPreferencesBoxViews"/>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ </prefpane>
+
+ <script type="application/javascript"
+ src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript"
+ src="chrome://lightning/content/messenger-overlay-preferences.js"/>
+
+ </prefwindow>
+
+</overlay>
diff --git a/calendar/lightning/content/messenger-overlay-sidebar.js b/calendar/lightning/content/messenger-overlay-sidebar.js
new file mode 100644
index 000000000..27ab090dc
--- /dev/null
+++ b/calendar/lightning/content/messenger-overlay-sidebar.js
@@ -0,0 +1,946 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported refreshUIBits, switchCalendarView, rescheduleInvitationsUpdate,
+ * openInvitationsDialog, onToolbarsPopupShowingWithMode,
+ * InitViewCalendarPaneMenu, onToolbarsPopupShowingForTabType,
+ * customizeMailToolbarForTabType
+ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+Components.utils.import("resource://calendar/modules/calAsyncUtils.jsm");
+
+var gLastShownCalendarView = null;
+
+var calendarTabMonitor = {
+ monitorName: "lightning",
+
+ // Unused, but needed functions
+ onTabTitleChanged: function() {},
+ onTabOpened: function() {},
+ onTabClosing: function() {},
+ onTabPersist: function() {},
+ onTabRestored: function() {},
+
+ onTabSwitched: function(aNewTab, aOldTab) {
+ // Unfortunately, tabmail doesn't provide a hideTab function on the tab
+ // type definitions. To make sure the commands are correctly disabled,
+ // we want to update calendar/task commands when switching away from
+ // those tabs.
+ if (aOldTab.mode.name == "calendar" ||
+ aOldTab.mode.name == "task") {
+ calendarController.updateCommands();
+ calendarController2.updateCommands();
+ }
+ }
+};
+
+var calendarTabType = {
+ name: "calendar",
+ panelId: "calendarTabPanel",
+ modes: {
+ calendar: {
+ type: "calendar",
+ maxTabs: 1,
+ openTab: function(aTab, aArgs) {
+ aTab.title = aArgs.title;
+ if (!("background" in aArgs) || !aArgs.background) {
+ // Only do calendar mode switching if the tab is opened in
+ // foreground.
+ ltnSwitch2Calendar();
+ }
+ },
+
+ showTab: function(aTab) {
+ ltnSwitch2Calendar();
+ },
+ closeTab: function(aTab) {
+ if (gCurrentMode == "calendar") {
+ // Only revert menu hacks if closing the active tab, otherwise we
+ // would switch to mail mode even if in task mode and closing the
+ // calendar tab.
+ ltnSwitch2Mail();
+ }
+ },
+
+ persistTab: function(aTab) {
+ let tabmail = document.getElementById("tabmail");
+ return {
+ // Since we do strange tab switching logic in ltnSwitch2Calendar,
+ // we should store the current tab state ourselves.
+ background: (aTab != tabmail.currentTabInfo)
+ };
+ },
+
+ restoreTab: function(aTabmail, aState) {
+ aState.title = ltnGetString("lightning", "tabTitleCalendar");
+ aTabmail.openTab("calendar", aState);
+ },
+
+ onTitleChanged: function(aTab) {
+ aTab.title = ltnGetString("lightning", "tabTitleCalendar");
+ },
+
+ supportsCommand: (aCommand, aTab) => calendarController2.supportsCommand(aCommand),
+ isCommandEnabled: (aCommand, aTab) => calendarController2.isCommandEnabled(aCommand),
+ doCommand: (aCommand, aTab) => calendarController2.doCommand(aCommand),
+ onEvent: (aEvent, aTab) => calendarController2.onEvent(aEvent)
+ },
+
+ tasks: {
+ type: "tasks",
+ maxTabs: 1,
+ openTab: function(aTab, aArgs) {
+ aTab.title = aArgs.title;
+ if (!("background" in aArgs) || !aArgs.background) {
+ ltnSwitch2Task();
+ }
+ },
+ showTab: function(aTab) {
+ ltnSwitch2Task();
+ },
+ closeTab: function(aTab) {
+ if (gCurrentMode == "task") {
+ // Only revert menu hacks if closing the active tab, otherwise we
+ // would switch to mail mode even if in calendar mode and closing the
+ // tasks tab.
+ ltnSwitch2Mail();
+ }
+ },
+
+ persistTab: function(aTab) {
+ let tabmail = document.getElementById("tabmail");
+ return {
+ // Since we do strange tab switching logic in ltnSwitch2Task,
+ // we should store the current tab state ourselves.
+ background: (aTab != tabmail.currentTabInfo)
+ };
+ },
+
+ restoreTab: function(aTabmail, aState) {
+ aState.title = ltnGetString("lightning", "tabTitleTasks");
+ aTabmail.openTab("tasks", aState);
+ },
+
+ onTitleChanged: function(aTab) {
+ aTab.title = ltnGetString("lightning", "tabTitleTasks");
+ },
+
+ supportsCommand: (aCommand, aTab) => calendarController2.supportsCommand(aCommand),
+ isCommandEnabled: (aCommand, aTab) => calendarController2.isCommandEnabled(aCommand),
+ doCommand: (aCommand, aTab) => calendarController2.doCommand(aCommand),
+ onEvent: (aEvent, aTab) => calendarController2.onEvent(aEvent)
+ }
+ },
+
+ /**
+ * Because calendar does some direct menu manipulation, we need to
+ * change to the mail mode to clean up after those hacks.
+ *
+ * @param {Object} aTab A tab info object
+ */
+ saveTabState: function(aTab) {
+ ltnSwitch2Mail();
+ }
+};
+
+/**
+ * For details about tab info objects and the tabmail interface see:
+ * comm-central/mail/base/content/mailTabs.js
+ * comm-central/mail/base/content/tabmail.xml
+ */
+var calendarItemTabType = {
+ name: "calendarItem",
+ perTabPanel: "vbox",
+ idNumber: 0,
+ modes: {
+ calendarEvent: { type: "calendarEvent" },
+ calendarTask: { type: "calendarTask" }
+ },
+ /**
+ * Opens an event tab or a task tab.
+ *
+ * @param {Object} aTab A tab info object
+ * @param {Object} aArgs Contains data about the event/task
+ */
+ openTab: function(aTab, aArgs) {
+ // Create a clone to use for this tab. Remove the cloned toolbox
+ // and move the original toolbox into its place. There is only
+ // one toolbox/toolbar so its settings are the same for all item tabs.
+ let original = document.getElementById("lightningItemPanel").firstChild;
+ let clone = original.cloneNode(true);
+
+ clone.querySelector("toolbox").remove();
+ moveEventToolbox(clone);
+ clone.setAttribute("id", "calendarItemTab" + this.idNumber);
+
+ if (aTab.mode.type == "calendarTask") {
+ // For task tabs, css class hides event-specific toolbar buttons.
+ clone.setAttribute("class", "calendar-task-dialog-tab");
+ }
+
+ aTab.panel.appendChild(clone);
+
+ // Set up the iframe and store the iframe's id. The iframe's
+ // src is set in onLoadLightningItemPanel() that is called below.
+ aTab.iframe = aTab.panel.querySelector("iframe");
+ let iframeId = "calendarItemTabIframe" + this.idNumber;
+ aTab.iframe.setAttribute("id", iframeId);
+ gItemTabIds.push(iframeId);
+
+ // Generate and set the tab title.
+ let strName;
+ if (aTab.mode.type == "calendarEvent") {
+ strName = aArgs.calendarEvent.title ? "editEventDialog" : "newEventDialog";
+ } else if (aTab.mode.type == "calendarTask") {
+ strName = aArgs.calendarEvent.title ? "editTaskDialog" : "newTaskDialog";
+ } else {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ }
+ // name is "New Event", "Edit Task", etc.
+ let name = cal.calGetString("calendar", strName);
+ aTab.title = name + ": " + (aArgs.calendarEvent.title || name);
+
+ // allowTabClose prevents the tab from being closed until we ask
+ // the user if they want to save any unsaved changes.
+ aTab.allowTabClose = false;
+
+ // Put the arguments where they can be accessed easily
+ // from the iframe. (window.arguments[0])
+ aTab.iframe.contentWindow.arguments = [aArgs];
+
+ // activate or de-activate 'Events and Tasks' menu items
+ document.commandDispatcher.updateCommands("calendar_commands");
+
+ onLoadLightningItemPanel(iframeId, aArgs.url);
+
+ this.idNumber += 1;
+ },
+ /**
+ * Saves a tab's state when it is deactivated / hidden. The opposite of showTab.
+ *
+ * @param {Object} aTab A tab info object
+ */
+ saveTabState: function(aTab) {
+ // save state
+ aTab.itemTabConfig = {};
+ Object.assign(aTab.itemTabConfig, gConfig);
+
+ // clear statusbar
+ let statusbar = document.getElementById("status-bar");
+ let items = statusbar.getElementsByClassName("event-dialog");
+ for (let item of items) {
+ item.setAttribute("collapsed", true);
+ }
+ // move toolbox to the place where it can be accessed later
+ let to = document.getElementById("lightningItemPanel").firstChild;
+ moveEventToolbox(to);
+ },
+ /**
+ * Called when a tab is activated / shown. The opposite of saveTabState.
+ *
+ * @param {Object} aTab A tab info object
+ */
+ showTab: function(aTab) {
+ // move toolbox into place then load state
+ moveEventToolbox(aTab.panel.firstChild);
+ Object.assign(gConfig, aTab.itemTabConfig);
+ updateItemTabState(gConfig);
+
+ // activate or de-activate 'Events and Tasks' menu items
+ document.commandDispatcher.updateCommands("calendar_commands");
+ },
+ /**
+ * Called when there is a request to close a tab. Using aTab.allowTabClose
+ * we first prevent the tab from closing so we can prompt the user
+ * about saving changes, then we allow the tab to close.
+ *
+ * @param {Object} aTab A tab info object
+ */
+ tryCloseTab: function(aTab) {
+ if (aTab.allowTabClose) {
+ return true;
+ } else {
+ onCancel(aTab.iframe.id);
+ return false;
+ }
+ },
+ /**
+ * Closes a tab.
+ *
+ * @param {Object} aTab A tab info object
+ */
+ closeTab: function(aTab) {
+ // Remove the iframe id from the array where they are stored.
+ let index = gItemTabIds.indexOf(aTab.iframe.id);
+ if (index != -1) {
+ gItemTabIds.splice(index, 1);
+ }
+ aTab.itemTabConfig = null;
+ },
+ /**
+ * Called when quitting the application (and/or closing the window).
+ * Saves an open tab's state to be able to restore it later.
+ *
+ * @param {Object} aTab A tab info object
+ */
+ persistTab: function(aTab) {
+ let args = aTab.iframe.contentWindow.arguments[0];
+ // Serialize args, with manual handling of some properties.
+ // persistTab is called even for new events/tasks in tabs that
+ // were closed and never saved (for 'undo close tab'
+ // functionality), thus we confirm we have the expected values.
+ if (!args || !args.calendar || !args.calendar.id ||
+ !args.calendarEvent || !args.calendarEvent.id) {
+ return {};
+ }
+
+ let calendarId = args.calendar.id;
+ let itemId = args.calendarEvent.id;
+ // Handle null args.initialStartDateValue, just for good measure.
+ // Note that this is not the start date for the event or task.
+ let hasDateValue = args.initialStartDateValue &&
+ args.initialStartDateValue.icalString;
+ let initialStartDate = hasDateValue
+ ? args.initialStartDateValue.icalString : null;
+
+ args.calendar = null;
+ args.calendarEvent = null;
+ args.initialStartDateValue = null;
+
+ return {
+ calendarId: calendarId,
+ itemId: itemId,
+ initialStartDate: initialStartDate,
+ args: args,
+ tabType: aTab.mode.type
+ };
+ },
+ /**
+ * Called when starting the application (and/or opening the window).
+ * Restores a tab that was open when the application was quit previously.
+ *
+ * @param {Object} aTabmail The tabmail interface
+ * @param {Object} aState The state of the tab to restore
+ */
+ restoreTab: function(aTabmail, aState) {
+ // Sometimes restoreTab is called for tabs that were never saved
+ // and never meant to be persisted or restored. See persistTab.
+ if (aState.args && aState.calendarId && aState.itemId) {
+ aState.args.initialStartDateValue = aState.initialStartDate
+ ? cal.createDateTime(aState.initialStartDate) : getDefaultStartDate();
+
+ aState.args.onOk = doTransaction.bind(null, "modify");
+
+ aState.args.calendar = getCalendarManager().getCalendarById(aState.calendarId);
+ if (aState.args.calendar) {
+ // using wrappedJSObject is a hack that is needed to prevent a proxy error
+ let pcal = cal.async.promisifyCalendar(aState.args.calendar.wrappedJSObject);
+ pcal.getItem(aState.itemId).then((item) => {
+ if (item[0]) {
+ aState.args.calendarEvent = item[0];
+ aTabmail.openTab(aState.tabType, aState.args);
+ }
+ });
+ }
+ }
+ }
+};
+
+window.addEventListener("load", (e) => {
+ let tabmail = document.getElementById("tabmail");
+ tabmail.registerTabType(calendarTabType);
+ tabmail.registerTabType(calendarItemTabType);
+ tabmail.registerTabMonitor(calendarTabMonitor);
+}, false);
+
+
+function ltnOnLoad(event) {
+ // nuke the onload, or we get called every time there's
+ // any load that occurs
+ window.removeEventListener("load", ltnOnLoad, false);
+
+ // Check if the binary component was loaded
+ checkCalendarBinaryComponent();
+
+ document.getElementById("calendarDisplayDeck")
+ .addEventListener("select", LtnObserveDisplayDeckChange, true);
+
+ // Take care of common initialization
+ commonInitCalendar();
+
+ // Add an unload function to the window so we don't leak any listeners
+ window.addEventListener("unload", ltnFinish, false);
+
+ // Set up invitations manager
+ scheduleInvitationsUpdate(FIRST_DELAY_STARTUP);
+ getCalendarManager().addObserver(gInvitationsCalendarManagerObserver);
+
+ let filter = document.getElementById("task-tree-filtergroup");
+ filter.value = filter.value || "all";
+ document.getElementById("modeBroadcaster").setAttribute("mode", gCurrentMode);
+ document.getElementById("modeBroadcaster").setAttribute("checked", "true");
+
+ let mailContextPopup = document.getElementById("mailContext");
+ if (mailContextPopup) {
+ mailContextPopup.addEventListener("popupshowing",
+ gCalSetupMailContext.popup, false);
+ }
+
+ // Setup customizeDone handlers for our toolbars
+ let toolbox = document.getElementById("calendar-toolbox");
+ toolbox.customizeDone = function(aEvent) {
+ MailToolboxCustomizeDone(aEvent, "CustomizeCalendarToolbar");
+ };
+ toolbox = document.getElementById("task-toolbox");
+ toolbox.customizeDone = function(aEvent) {
+ MailToolboxCustomizeDone(aEvent, "CustomizeTaskToolbar");
+ };
+
+ ltnIntegrationCheck();
+
+ Services.obs.notifyObservers(window, "lightning-startup-done", false);
+}
+
+/**
+ * Displays the Lightning integration notification bar
+ */
+function ltnIntegrationNotification() {
+ const kOptOut = "mail.calendar-integration.opt-out"; // default: false
+ const kNotify = "calendar.integration.notify"; // default: true
+ const kSupportUri = "https://support.mozilla.org/kb/thunderbird-calendar-integration";
+ const kLightningGuuid = "{e2fda1a4-762b-4020-b5ad-a41df1933103}";
+
+ // we fall back to messagepanebox for Seamonkey
+ let notifyBox = document.getElementById("mail-notification-box") ||
+ document.getElementById("messagepanebox");
+
+ let appBrand = cal.calGetString("brand", "brandShortName", null, "branding");
+ let ltnBrand = ltnGetString("lightning", "brandShortName");
+ let label = ltnGetString("lightning", "integrationLabel", [appBrand, ltnBrand]);
+
+ // call backs for doing/undoing Lightning removal
+ let cbRemoveLightning = function(aAddon) {
+ aAddon.userDisabled = true;
+ };
+ let cbUndoRemoveLightning = function(aAddon) {
+ aAddon.userDisabled = false;
+ };
+
+ // call backs for the undo opt-out bar
+ let cbRestartNow = function(aNotificationBar, aButton) {
+ Services.startup.quit(Components.interfaces.nsIAppStartup.eRestart |
+ Components.interfaces.nsIAppStartup.eForceQuit);
+ };
+ let cbUndoOptOut = function(aNotificationBar, aButton) {
+ Preferences.set(kNotify, true);
+ Preferences.set(kOptOut, false);
+ AddonManager.getAddonByID(kLightningGuuid, cbUndoRemoveLightning);
+ // display notification bar again
+ ltnIntegrationNotification();
+ };
+
+ // call backs for the opt-out bar
+ let cbLearnMore = function(aNotificationBar, aButton) {
+ // In SeaMonkey the second parameter should be either null or an
+ // event object with a non null target.ownerDocument.
+ openUILink(kSupportUri, {
+ button: 0,
+ target: { ownerDocument: document }
+ });
+ return true;
+ };
+ let cbKeepIt = function(aNotificationBar, aButton) {
+ Preferences.set(kNotify, false);
+ };
+ let cbOptOut = function(aNotificationBar, aButton) {
+ Preferences.set(kNotify, false);
+ Preferences.set(kOptOut, true);
+ AddonManager.getAddonByID(kLightningGuuid, cbRemoveLightning);
+ // let the user know that removal will be applied after restart
+ let restartLabel = ltnGetString("lightning", "integrationRestartLabel", [ltnBrand, appBrand]);
+ let button = [{
+ label: ltnGetString("lightning", "integrationUndoButton"),
+ accessKey: ltnGetString("lightning", "integrationUndoAccessKey"),
+ popup: null,
+ callback: cbUndoOptOut
+ }, {
+ label: ltnGetString("lightning", "integrationRestartButton"),
+ accessKey: ltnGetString("lightning", "integrationRestartAccessKey"),
+ popup: null,
+ callback: cbRestartNow
+ }];
+ notifyBox.appendNotification(restartLabel,
+ "restart-required",
+ null,
+ notifyBox.PRIORITY_INFO_MEDIUM,
+ button);
+ };
+
+ let buttons = [{
+ label: ltnGetString("lightning", "integrationLearnMoreButton"),
+ accessKey: ltnGetString("lightning", "integrationLearnMoreAccessKey"),
+ popup: null,
+ callback: cbLearnMore
+ }, {
+ label: ltnGetString("lightning", "integrationOptOutButton"),
+ accessKey: ltnGetString("lightning", "integrationOptOutAccessKey"),
+ popup: null,
+ callback: cbOptOut
+ }, {
+ label: ltnGetString("lightning", "integrationKeepItButton"),
+ accessKey: ltnGetString("lightning", "integrationKeepItAccessKey"),
+ popup: null,
+ callback: cbKeepIt
+ }];
+
+ // we use PRIORITY_INFO_MEDIUM to overrule notifications from specialTabs.js if any
+ let notification = notifyBox.appendNotification(label,
+ "calendar-integration",
+ null,
+ notifyBox.PRIORITY_INFO_MEDIUM,
+ buttons);
+ notification.persistence = 3;
+}
+
+/**
+ * Checks whether to display the opt-out notification for Lightning integration
+ */
+function ltnIntegrationCheck() {
+ const kOptOut = "mail.calendar-integration.opt-out"; // default: false
+ const kNotify = "calendar.integration.notify"; // default: true
+ // don't do anything if the opt-out pref doesn't exist or is enabled by the user or the user has
+ // already decided to keep Lightning
+ if (!Preferences.get(kOptOut, true) && Preferences.get(kNotify, false)) {
+ // action is only needed, if hasn't used Lightning before, so lets check whether this looks
+ // like a default calendar setup
+ let cnt = {};
+ let calMgr = cal.getCalendarManager();
+ let cals = calMgr.getCalendars(cnt);
+ let homeCalName = cal.calGetString("calendar", "homeCalendarName", null, "calendar");
+ if (cnt.value == 1 &&
+ calMgr.getCalendarPref_(cals[0], "type") == "storage" &&
+ calMgr.getCalendarPref_(cals[0], "name") == homeCalName) {
+ // this looks like a default setup, so let's see whether the calendar contains any items
+ let pCal = cal.async.promisifyCalendar(cals[0]);
+ // we look at all items at any time, but we can stop if the first item was found
+ // if we've found no items, we call ltnIntegrationNotification to display the bar
+ pCal.getItems(Components.interfaces.calICalendar.ITEM_FILTER_ALL_ITEMS, 1, null, null)
+ .then((aItems) => { if (!aItems.length) { ltnIntegrationNotification(); } });
+ }
+ }
+}
+
+/* Called at midnight to tell us to redraw date-specific widgets. Do NOT call
+ * this for normal refresh, since it also calls scheduleMidnightRefresh.
+ */
+function refreshUIBits() {
+ try {
+ getMinimonth().refreshDisplay();
+
+ // Refresh the current view and just allow the refresh for the others
+ // views when will be displayed.
+ let currView = currentView();
+ currView.goToDay();
+ ["day-view",
+ "week-view",
+ "multiweek-view",
+ "month-view"].forEach((view) => {
+ if (view != currView.id) {
+ document.getElementById(view).mToggleStatus = -1;
+ }
+ });
+
+ if (!TodayPane.showsToday()) {
+ TodayPane.setDay(now());
+ }
+
+ // update the unifinder
+ refreshEventTree();
+
+ // update today's date on todaypane button
+ document.getElementById("calendar-status-todaypane-button").setUpTodayDate();
+ } catch (exc) {
+ ASSERT(false, exc);
+ }
+
+ // schedule our next update...
+ scheduleMidnightUpdate(refreshUIBits);
+}
+
+/**
+ * Switch the calendar view, and optionally switch to calendar mode.
+ *
+ * @param aType The type of view to select.
+ * @param aShow If true, the mode will be switched to calendar if not
+ * already there.
+ */
+function switchCalendarView(aType, aShow) {
+ gLastShownCalendarView = aType;
+
+ if (aShow && gCurrentMode != "calendar") {
+ // This function in turn calls switchToView(), so return afterwards.
+ ltnSwitch2Calendar();
+ return;
+ }
+
+ // Sunbird/Lightning common view switching code
+ switchToView(aType);
+}
+
+/**
+ * This function has the sole responsibility to switch back to
+ * mail mode (by calling ltnSwitch2Mail()) if we are getting
+ * notifications from other panels (besides the calendar views)
+ * but find out that we're not in mail mode. This situation can
+ * for example happen if we're in calendar mode but the 'new mail'
+ * slider gets clicked and wants to display the appropriate mail.
+ * All necessary logic for switching between the different modes
+ * should live inside of the corresponding functions:
+ * - ltnSwitch2Mail()
+ * - ltnSwitch2Calendar()
+ * - ltnSwitch2Task()
+ */
+function LtnObserveDisplayDeckChange(event) {
+ let deck = event.target;
+
+ // Bug 309505: The 'select' event also fires when we change the selected
+ // panel of calendar-view-box. Workaround with this check.
+ if (deck.id != "calendarDisplayDeck") {
+ return;
+ }
+
+ let id = deck.selectedPanel && deck.selectedPanel.id;
+
+ // Switch back to mail mode in case we find that this
+ // notification has been fired but we're still in calendar or task mode.
+ // Specifically, switch back if we're *not* in mail mode but the notification
+ // did *not* come from either the "calendar-view-box" or the "calendar-task-box".
+ if (gCurrentMode != "mail") {
+ if (id != "calendar-view-box" && id != "calendar-task-box") {
+ ltnSwitch2Mail();
+ }
+ }
+}
+
+function ltnFinish() {
+ getCalendarManager().removeObserver(gInvitationsCalendarManagerObserver);
+
+ // Remove listener for mailContext.
+ let mailContextPopup = document.getElementById("mailContext");
+ if (mailContextPopup) {
+ mailContextPopup.removeEventListener("popupshowing",
+ gCalSetupMailContext.popup, false);
+ }
+
+ // Common finish steps
+ commonFinishCalendar();
+}
+
+// == invitations link
+var FIRST_DELAY_STARTUP = 100;
+var FIRST_DELAY_RESCHEDULE = 100;
+var FIRST_DELAY_REGISTER = 10000;
+var FIRST_DELAY_UNREGISTER = 0;
+
+var gInvitationsOperationListener = {
+ mCount: 0,
+
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calIOperationListener]),
+ onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDetail) {
+ let invitationsBox = document.getElementById("calendar-invitations-panel");
+ if (Components.isSuccessCode(aStatus)) {
+ let value = ltnGetString("lightning", "invitationsLink.label", [this.mCount]);
+ document.getElementById("calendar-invitations-label").value = value;
+ setElementValue(invitationsBox, this.mCount < 1 && "true", "hidden");
+ } else {
+ invitationsBox.setAttribute("hidden", "true");
+ }
+ this.mCount = 0;
+ },
+
+ onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
+ if (Components.isSuccessCode(aStatus)) {
+ this.mCount += aCount;
+ }
+ }
+};
+
+var gInvitationsCalendarManagerObserver = {
+ mSideBar: this,
+
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.calICalendarManagerObserver]),
+
+ onCalendarRegistered: function(aCalendar) {
+ this.mSideBar.rescheduleInvitationsUpdate(FIRST_DELAY_REGISTER);
+ },
+
+ onCalendarUnregistering: function(aCalendar) {
+ this.mSideBar.rescheduleInvitationsUpdate(FIRST_DELAY_UNREGISTER);
+ },
+
+ onCalendarDeleting: function(aCalendar) {
+ }
+};
+
+function scheduleInvitationsUpdate(firstDelay) {
+ gInvitationsOperationListener.mCount = 0;
+ getInvitationsManager().scheduleInvitationsUpdate(firstDelay,
+ gInvitationsOperationListener);
+}
+
+function rescheduleInvitationsUpdate(firstDelay) {
+ getInvitationsManager().cancelInvitationsUpdate();
+ scheduleInvitationsUpdate(firstDelay);
+}
+
+function openInvitationsDialog() {
+ getInvitationsManager().cancelInvitationsUpdate();
+ gInvitationsOperationListener.mCount = 0;
+ getInvitationsManager().openInvitationsDialog(
+ gInvitationsOperationListener,
+ () => scheduleInvitationsUpdate(FIRST_DELAY_RESCHEDULE));
+}
+
+/**
+ * the current mode is set to a string defining the current
+ * mode we're in. allowed values are:
+ * - 'mail'
+ * - 'calendar'
+ * - 'task'
+ */
+var gCurrentMode = "mail";
+
+/**
+ * ltnSwitch2Mail() switches to the mail mode
+ */
+
+function ltnSwitch2Mail() {
+ if (gCurrentMode != "mail") {
+ gCurrentMode = "mail";
+ document.getElementById("modeBroadcaster").setAttribute("mode", gCurrentMode);
+
+ document.commandDispatcher.updateCommands("calendar_commands");
+ window.setCursor("auto");
+ }
+}
+
+/**
+ * ltnSwitch2Calendar() switches to the calendar mode
+ */
+
+function ltnSwitch2Calendar() {
+ if (gCurrentMode != "calendar") {
+ gCurrentMode = "calendar";
+ document.getElementById("modeBroadcaster").setAttribute("mode", gCurrentMode);
+
+ // display the calendar panel on the display deck
+ let deck = document.getElementById("calendarDisplayDeck");
+ deck.selectedPanel = document.getElementById("calendar-view-box");
+
+ // show the last displayed type of calendar view
+ switchToView(gLastShownCalendarView);
+
+ document.commandDispatcher.updateCommands("calendar_commands");
+ window.setCursor("auto");
+
+ // make sure the view is sized correctly
+ onCalendarViewResize();
+ }
+}
+
+/**
+ * ltnSwitch2Task() switches to the task mode
+ */
+
+function ltnSwitch2Task() {
+ if (gCurrentMode != "task") {
+ gCurrentMode = "task";
+ document.getElementById("modeBroadcaster").setAttribute("mode", gCurrentMode);
+
+ // display the task panel on the display deck
+ let deck = document.getElementById("calendarDisplayDeck");
+ deck.selectedPanel = document.getElementById("calendar-task-box");
+
+ document.commandDispatcher.updateCommands("calendar_commands");
+ window.setCursor("auto");
+ }
+}
+
+var gCalSetupMailContext = {
+ popup: function() {
+ let hasSelection = (gFolderDisplay.selectedMessage != null);
+ // Disable the convert menu altogether.
+ setElementValue("mailContext-calendar-convert-menu",
+ !hasSelection && "true", "hidden");
+ }
+};
+
+// Overwrite the InitMessageMenu function, since we never know in which order
+// the popupshowing event will be processed. This function takes care of
+// disabling the message menu when in calendar or task mode.
+function calInitMessageMenu() {
+ calInitMessageMenu.origFunc();
+
+ document.getElementById("markMenu").disabled = (gCurrentMode != "mail");
+}
+calInitMessageMenu.origFunc = InitMessageMenu;
+InitMessageMenu = calInitMessageMenu;
+
+window.addEventListener("load", ltnOnLoad, false);
+
+/**
+ * Get the toolbox id for the current tab type.
+ *
+ * @return {string} A toolbox id or null
+ */
+function getToolboxIdForCurrentTabType() {
+ // A mapping from calendar tab types to toolbox ids.
+ const calendarToolboxIds = {
+ calendar: "calendar-toolbox",
+ tasks: "task-toolbox",
+ calendarEvent: "event-toolbox",
+ calendarTask: "event-toolbox"
+ };
+ let tabmail = document.getElementById("tabmail");
+ let tabType = tabmail.currentTabInfo.mode.type;
+
+ return calendarToolboxIds[tabType] || "mail-toolbox";
+}
+
+/**
+ * Modify the contents of the "Toolbars" context menu for the current
+ * tab type. Menu items are inserted before (appear above) aInsertPoint.
+ *
+ * @param {MouseEvent} aEvent The popupshowing event
+ * @param {nsIDOMXULElement} aInsertPoint (optional) menuitem node
+ */
+function onToolbarsPopupShowingForTabType(aEvent, aInsertPoint) {
+ if (onViewToolbarsPopupShowing.length < 3) {
+ // SeaMonkey
+ onViewToolbarsPopupShowing(aEvent);
+ return;
+ }
+
+ let toolboxes = [];
+ let toolboxId = getToolboxIdForCurrentTabType();
+
+ // We add navigation-toolbox ("Menu Bar") for all tab types except
+ // mail tabs because mail-toolbox already includes navigation-toolbox,
+ // so we do not need to add it separately in that case.
+ if (toolboxId != "mail-toolbox") {
+ toolboxes.push("navigation-toolbox");
+ }
+ toolboxes.push(toolboxId);
+
+ if (toolboxId == "event-toolbox") {
+ // Clear the event/task tab's toolbox.externalToolbars to prevent
+ // duplicate entries for its toolbar in "Toolbars" menu.
+ // (The cloning and/or moving of this toolbox and toolbar on
+ // openTab causes the toolbar to be added to
+ // toolbox.externalToolbars, in addition to being a child node
+ // of the toolbox, leading to duplicate menu entries.)
+ let eventToolbox = document.getElementById("event-toolbox");
+ if (eventToolbox) {
+ eventToolbox.externalToolbars = [];
+ }
+ }
+
+ onViewToolbarsPopupShowing(aEvent, toolboxes, aInsertPoint);
+}
+
+/**
+ * Open the customize dialog for the toolbar for the current tab type.
+ */
+function customizeMailToolbarForTabType() {
+ let toolboxId = getToolboxIdForCurrentTabType();
+ if (toolboxId == "event-toolbox") {
+ onCommandCustomize();
+ } else {
+ CustomizeMailToolbar(toolboxId, "CustomizeMailToolbar");
+ }
+}
+
+// Initialize the Calendar sidebar menu state
+function InitViewCalendarPaneMenu() {
+ let calSidebar = document.getElementById("ltnSidebar");
+
+ setBooleanAttribute("ltnViewCalendarPane", "checked",
+ !calSidebar.getAttribute("collapsed"));
+
+ if (document.getElementById("appmenu_ltnViewCalendarPane")) {
+ setBooleanAttribute("appmenu_ltnViewCalendarPane", "checked",
+ !calSidebar.getAttribute("collapsed"));
+ }
+}
+
+
+/**
+ * Move the event toolbox, containing the toolbar, into view for a tab
+ * or back to its hiding place where it is accessed again for other tabs.
+ *
+ * @param {nsIDOMNode} aDestination Destination where the toolbox will be moved
+ */
+function moveEventToolbox(aDestination) {
+ let toolbox = document.getElementById("event-toolbox");
+ // the <toolbarpalette> has to be copied manually
+ let palette = toolbox.palette;
+ let iframe = aDestination.querySelector("iframe");
+ aDestination.insertBefore(toolbox, iframe);
+ toolbox.palette = palette;
+}
+
+/**
+ * Checks if Lightning's binary component was successfully loaded.
+ */
+function checkCalendarBinaryComponent() {
+ // Don't even get started if we are running ical.js or the binary component
+ // was successfully loaded.
+ if ("@mozilla.org/calendar/datetime;1" in Components.classes ||
+ Preferences.get("calendar.icaljs", false)) {
+ return;
+ }
+
+ const THUNDERBIRD_GUID = "{3550f703-e582-4d05-9a08-453d09bdfdc6}";
+ const SEAMONKEY_GUID = "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}";
+ const LIGHTNING_GUID = "{e2fda1a4-762b-4020-b5ad-a41df1933103}";
+
+ AddonManager.getAddonByID(LIGHTNING_GUID, (ext) => {
+ if (!ext) {
+ return;
+ }
+
+ let version;
+ let appversion = Services.appinfo.version;
+ let versionparts = appversion.split(".");
+ let extbrand = ltnGetString("lightning", "brandShortName");
+
+ switch (Services.appinfo.ID) {
+ case THUNDERBIRD_GUID: // e.g. 31.4.0 -> 3.3
+ version = ((parseInt(versionparts[0], 10) + 2) / 10).toFixed(1);
+ break;
+ case SEAMONKEY_GUID: // e.g. 2.28.4 -> 3.3
+ version = ((parseInt(versionparts[1], 10) + 5) / 10).toFixed(1);
+ break;
+ }
+
+ let text;
+ if (version && version != ext.version) {
+ let args = [extbrand, ext.version, version];
+ text = ltnGetString("lightning", "binaryComponentKnown", args);
+ } else {
+ let brand = cal.calGetString("brand", "brandShortName", null, "branding");
+ let args = [extbrand, brand, appversion, ext.version];
+ text = ltnGetString("lightning", "binaryComponentUnknown", args);
+ }
+
+ let title = ltnGetString("lightning", "binaryComponentTitle", [extbrand]);
+ openAddonsMgr("addons://detail/" + encodeURIComponent(LIGHTNING_GUID));
+ Services.prompt.alert(window, title, text);
+ });
+}
diff --git a/calendar/lightning/content/messenger-overlay-sidebar.xul b/calendar/lightning/content/messenger-overlay-sidebar.xul
new file mode 100644
index 000000000..b00dcef73
--- /dev/null
+++ b/calendar/lightning/content/messenger-overlay-sidebar.xul
@@ -0,0 +1,315 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE overlay
+[
+ <!ENTITY % dtd1 SYSTEM "chrome://lightning/locale/lightning.dtd" > %dtd1;
+ <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/menuOverlay.dtd" > %dtd2;
+ <!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd3;
+ <!ENTITY % dtd4 SYSTEM "chrome://lightning/locale/lightning-toolbar.dtd" > %dtd4;
+ <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" > %messengerDTD;
+ <!ENTITY % eventDialogDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd" > %eventDialogDTD;
+]>
+
+<?xml-stylesheet href="chrome://lightning/skin/lightning.css" type="text/css"?>
+
+<?xml-stylesheet href="chrome://calendar/content/calendar-view-bindings.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/content/datetimepickers/datetimepickers.css" type="text/css"?>
+
+<?xml-stylesheet href="chrome://calendar/skin/calendar-event-dialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/content/calendar-event-dialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar-common/skin/dialogs/calendar-event-dialog.css" type="text/css"?>
+
+<?xul-overlay href="chrome://calendar/content/calendar-calendars-list.xul"?>
+<?xul-overlay href="chrome://calendar/content/calendar-common-sets.xul"?>
+<?xul-overlay href="chrome://calendar/content/calendar-views.xul"?>
+
+<?xul-overlay href="chrome://lightning/content/lightning-toolbar.xul"?>
+<?xul-overlay href="chrome://lightning/content/lightning-menus.xul"?>
+
+<overlay id="ltnSidebarOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <!-- NEEDED FOR MULTIPLE CALENDAR SUPPORT -->
+ <script type="application/javascript" src="chrome://calendar/content/calendar-management.js"/>
+
+ <!-- NEEDED FOR CLIPBOARD SUPPORT -->
+ <script type="application/javascript" src="chrome://calendar/content/calendar-clipboard.js"/>
+
+ <!-- NEEDED FOR IMPORT / EXPORT SUPPORT -->
+ <script type="application/javascript" src="chrome://calendar/content/import-export.js"/>
+
+ <!-- NEEDED FOR PUBLICATION SUPPORT -->
+ <script type="application/javascript" src="chrome://calendar/content/publish.js"/>
+
+ <script type="application/javascript" src="chrome://calendar/content/calendar-item-editing.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-chrome-startup.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calUtils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/mouseoverPreviews.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-views.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-ui-utils.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-creation.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-dnd-listener.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-statusbar.js"/>
+ <script type="application/javascript" src="chrome://global/content/nsDragAndDrop.js"/>
+
+ <!-- NEEDED FOR TASK VIEW/LIST SUPPORT -->
+ <script type="application/javascript" src="chrome://calendar/content/calendar-task-editing.js"/>
+
+ <script type="application/javascript" src="chrome://calendar/content/calendar-extract.js"/>
+
+ <script type="application/javascript" src="chrome://lightning/content/lightning-utils.js"/>
+ <script type="application/javascript" src="chrome://lightning/content/messenger-overlay-sidebar.js"/>
+ <script type="application/javascript" src="chrome://calendar/content/calendar-invitations-manager.js"/>
+
+ <!-- NEEDED FOR EVENT/TASK IN A TAB -->
+ <script type="application/javascript" src="chrome://lightning/content/lightning-item-panel.js"/>
+
+ <window id="messengerWindow">
+ <!-- Be sure to keep these sets, since they will be overlayed by
+ calendar/base/content/calendar-common-sets.xul -->
+ <commandset id="calendar_commands">
+ <command id="agenda_delete_event_command" oncommand="agendaListbox.deleteSelectedItem(false);"/>
+ <command id="agenda_edit_event_command" oncommand="agendaListbox.editSelectedItem(event);"/>
+ <command id="switch2calendar"
+ oncommand="document.getElementById('tabmail').openTab('calendar', { title: document.getElementById('calendar-tab-button').getAttribute('title') })"/>
+ <command id="switch2task"
+ oncommand="document.getElementById('tabmail').openTab('tasks', { title: document.getElementById('task-tab-button').getAttribute('title') })"/>
+ <command id="new_calendar_tab"
+ oncommand="document.getElementById('tabmail').openTab('calendar', { title: document.getElementById('calendar-tab-button').getAttribute('title') })"/>
+ <command id="new_task_tab"
+ oncommand="document.getElementById('tabmail').openTab('tasks', { title: document.getElementById('task-tab-button').getAttribute('title') })"/>
+ <command id="calendar_go_to_today_command"
+ observes="calendar_mode_calendar"
+ oncommand="document.getElementById('tabmail').openTab('calendar', { title: document.getElementById('calendar-tab-button').getAttribute('title') }); goToDate(now())"/>
+ </commandset>
+
+ <commandset id="mailCommands">
+ <command id="cmd_CustomizeMailToolbar"
+ oncommand="customizeMailToolbarForTabType()"/>
+ </commandset>
+
+ <keyset id="calendar-keys">
+ <key id="openLightningKey"
+ key="&lightning.keys.event.showCalendar.key;"
+ modifiers="accel, shift"
+ observes="new_calendar_tab"/>
+ <key id="openTasksKey"
+ key="&lightning.keys.event.showTasks.key;"
+ modifiers="accel, shift"
+ command="new_task_tab"/>
+ <key id="todaypanekey" command="calendar_toggle_todaypane_command" keycode="VK_F11"/>
+ <key id="calendar-new-event-key" key="&lightning.keys.event.new;" modifiers="accel" command="calendar_new_event_command"/>
+ <key id="calendar-new-todo-key" key="&lightning.keys.todo.new;" modifiers="accel" command="calendar_new_todo_command"/>
+ </keyset>
+
+ <broadcasterset id="calendar_broadcasters">
+ <broadcaster id="filterBroadcaster" value="all"/>
+ </broadcasterset>
+
+ <popupset id="calendar-popupset"/>
+ </window>
+
+ <toolbar id="tabs-toolbar">
+ <toolbarbutton id="calendar-tab-button"
+ class="toolbarbutton-1"
+ insertafter="alltabs-button"
+ title="&lightning.toolbar.calendar.label;"
+ tooltiptext="&lightning.toolbar.calendar.tooltip;"
+ command="new_calendar_tab"
+ hidden="true"/>
+ <toolbarbutton id="task-tab-button"
+ class="toolbarbutton-1"
+ insertafter="calendar-tab-button"
+ title="&lightning.toolbar.task.label;"
+ tooltiptext="&lightning.toolbar.task.tooltip;"
+ command="new_task_tab"
+ hidden="true"/>
+ </toolbar>
+
+ <tabpanels id="tabpanelcontainer">
+ <vbox id="calendarTabPanel">
+ <!-- Unfortunately we use the same panel for task and calendar tabs, so
+ we need to differ which toolbar is being shown. The actual toolbar
+ content will be added via a further overlay -->
+ <modevbox id="calendar-toolbox-container" mode="calendar" broadcaster="modeBroadcaster">
+ <toolbox id="calendar-toolbox"/>
+ </modevbox>
+ <modevbox id="task-toolbox-container" mode="task" broadcaster="modeBroadcaster">
+ <toolbox id="task-toolbox"/>
+ </modevbox>
+ <hbox id="calendarContent" flex="1">
+ <vbox id="ltnSidebar"
+ width="200"
+ persist="collapsed width">
+ <modevbox id="minimonth-pane" mode="calendar,task" broadcaster="modeBroadcaster" refcontrol="calendar_toggle_minimonthpane_command">
+ <vbox align="center">
+ <hbox id="calMinimonthBox" pack="center">
+ <minimonth id="calMinimonth" onchange="minimonthPick(this.value);" freebusy="true"/>
+ </hbox>
+ </vbox>
+ </modevbox>
+ <separator id="minimonth-splitter" minwidth="100"/>
+ <vbox id="calendar-panel" flex="1">
+ <modevbox id="task-filter-pane" mode="task" broadcaster="modeBroadcaster" refcontrol="calendar_toggle_filter_command">
+ <treenode-checkbox id="task-tree-filter-header"
+ checked="true"
+ class="treenode-checkbox"
+ label="&calendar.task.filter.title.label;"/>
+ <modevbox id="task-filtertree-pane" flex="1" mode="task" broadcaster="modeBroadcaster" refcontrol="task-tree-filter-header">
+ <radiogroup id="task-tree-filtergroup" class="task-tree-subpane"
+ persist="value">
+ <observes element="filterBroadcaster"
+ attribute="value"
+ onbroadcast="checkRadioControl(this.parentNode, document.getElementById('filterBroadcaster').getAttribute('value'));"/>
+ <radio id="opt_throughcurrent_filter" label="&calendar.task.filter.current.label;" value="throughcurrent" command="calendar_task_filter_command"/>
+ <radio id="opt_today_filter" label="&calendar.task.filter.today.label;" value="throughtoday" command="calendar_task_filter_command"/>
+ <radio id="opt_next7days_filter" label="&calendar.task.filter.next7days.label;" value="throughsevendays" command="calendar_task_filter_command"/>
+ <radio id="opt_notstarted_filter" label="&calendar.task.filter.notstarted.label;" value="notstarted" command="calendar_task_filter_command"/>
+ <radio id="opt_overdue_filter" label="&calendar.task.filter.overdue.label;" value="overdue" command="calendar_task_filter_command"/>
+ <radio id="opt_completed_filter" label="&calendar.task.filter.completed.label;" value="completed" command="calendar_task_filter_command"/>
+ <radio id="opt_open_filter" label="&calendar.task.filter.open.label;" value="open" command="calendar_task_filter_command"/>
+ <radio id="opt_all_filter" label="&calendar.task.filter.all.label;" value="all" command="calendar_task_filter_command"/>
+ </radiogroup>
+ </modevbox>
+ </modevbox>
+ <modevbox id="calendar-list-pane" flex="1" mode="calendar,task" broadcaster="modeBroadcaster"
+ refcontrol="calendar_toggle_calendarlist_command">
+ <treenode-checkbox id="calendar-list-header"
+ checked="true"
+ class="treenode-checkbox"
+ ondrop="return document.getElementById('calendar-list-tree-widget').foreignDrop(event)"
+ ondragenter="return document.getElementById('calendar-list-tree-widget').foreignCanDrop(event)"
+ ondragover="return document.getElementById('calendar-list-tree-widget').foreignCanDrop(event)"
+ label="&calendar.list.header.label;"/>
+ <modevbox id="calendar-listtree-pane" flex="1" mode="calendar,task" broadcaster="modeBroadcaster"
+ refcontrol="calendar-list-header">
+
+ <calendar-list-tree id="calendar-list-tree-widget"
+ class="task-tree-subpane"
+ flex="1"/>
+ </modevbox>
+ </modevbox>
+ </vbox>
+ </vbox>
+
+ <splitter id="calsidebar_splitter"
+ collapse="before"
+ persist="state"
+ class="calendar-sidebar-splitter"/>
+
+ <deck id="calendarDisplayDeck" flex="1">
+ <!-- vbox "calendar-view-box will be overlayed..." -->
+ <vbox id="calendar-view-box"/>
+ </deck>
+ </hbox>
+ </vbox>
+ </tabpanels>
+
+ <hbox id="tabmail-container">
+ <splitter id="today-splitter"
+ collapse="after"
+ resizebefore="closest"
+ state="collapsed"
+ class="calendar-sidebar-splitter"
+ oncommand="TodayPane.onCommandTodaySplitter();">
+ <grippy/>
+ </splitter>
+ <modevbox id="today-pane-panel" />
+ </hbox>
+
+ <statusbar id="status-bar">
+ <!-- event/task in tab statusbarpanels -->
+ <statusbarpanel id="status-privacy"
+ class="event-dialog"
+ align="center"
+ flex="1"
+ collapsed="true"
+ pack="start">
+ <label value="&event.statusbarpanel.privacy.label;"/>
+ <hbox id="status-privacy-public-box" privacy="PUBLIC">
+ <label value="&event.menu.options.privacy.public.label;"/>
+ </hbox>
+ <hbox id="status-privacy-confidential-box" privacy="CONFIDENTIAL">
+ <label value="&event.menu.options.privacy.confidential.label;"/>
+ </hbox>
+ <hbox id="status-privacy-private-box" privacy="PRIVATE">
+ <label value="&event.menu.options.privacy.private.label;"/>
+ </hbox>
+ </statusbarpanel>
+ <statusbarpanel id="status-priority"
+ class="event-dialog"
+ align="center"
+ flex="1"
+ collapsed="true"
+ pack="start">
+ <label value="&event.priority2.label;"/>
+ <image id="image-priority-low"
+ class="cal-statusbar-1"
+ collapsed="true"
+ value="low"/>
+ <image id="image-priority-normal"
+ class="cal-statusbar-1"
+ collapsed="true"
+ value="normal"/>
+ <image id="image-priority-high"
+ class="cal-statusbar-1"
+ collapsed="true"
+ value="high"/>
+ </statusbarpanel>
+ <statusbarpanel id="status-status"
+ class="event-dialog"
+ align="center"
+ flex="1"
+ collapsed="true"
+ pack="start">
+ <label value="&task.status.label;"/>
+ <label id="status-status-tentative-label"
+ value="&newevent.status.tentative.label;"
+ hidden="true"/>
+ <label id="status-status-confirmed-label"
+ value="&newevent.status.confirmed.label;"
+ hidden="true"/>
+ <label id="status-status-cancelled-label"
+ value="&newevent.eventStatus.cancelled.label;"
+ hidden="true"/>
+ </statusbarpanel>
+ <statusbarpanel id="status-freebusy"
+ class="event-only event-dialog"
+ align="center"
+ flex="1"
+ collapsed="true"
+ pack="start">
+ <label value="&event.statusbarpanel.freebusy.label;"/>
+ <label id="status-freebusy-free-label"
+ value="&event.freebusy.legend.free;"
+ hidden="true"/>
+ <label id="status-freebusy-busy-label"
+ value="&event.freebusy.legend.busy;"
+ hidden="true"/>
+ </statusbarpanel>
+ <!-- end event/task in tab statusbarpanels -->
+ <statusbarpanel id="calendar-show-todaypane-panel"
+ pack="center">
+ <toolbarbutton id="calendar-status-todaypane-button"
+ todaypane="true"
+ type="checkbox"
+ label="&todaypane.statusButton.label;"
+ tooltiptext="&calendar.todaypane.button.tooltip;"
+ observes="calendar_toggle_todaypane_command"
+ command="calendar_toggle_todaypane_command"/>
+ </statusbarpanel>
+ <statusbarpanel id="calendar-invitations-panel"
+ insertbefore="unreadMessageCount,totalMessageCount"
+ oncommand="openInvitationsDialog()">
+ <label id="calendar-invitations-label"
+ class="text-link"
+ onclick="openInvitationsDialog()"
+ onkeypress="if (event.keyCode == event.VK_RETURN) {
+ openInvitationsDialog(); }"/>
+ </statusbarpanel>
+ </statusbar>
+</overlay>
diff --git a/calendar/lightning/content/suite-overlay-addons.xul b/calendar/lightning/content/suite-overlay-addons.xul
new file mode 100644
index 000000000..975a0835e
--- /dev/null
+++ b/calendar/lightning/content/suite-overlay-addons.xul
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<overlay id="suiteAddonsOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"><![CDATA[
+ var lightningPrefs = {
+ guid: "{e2fda1a4-762b-4020-b5ad-a41df1933103}",
+ handleEvent: function(aEvent) {
+ var item = gListView.getListItemForID(this.guid);
+ if (!item)
+ return;
+
+ item.showPreferences = this.showPreferences;
+ },
+ showPreferences: function() {
+ var win = Services.wm.getMostRecentWindow("mozilla:preferences");
+ if (win) {
+ win.focus();
+ var doc = win.document;
+ var pane = doc.getElementById("paneLightning");
+ doc.documentElement.syncTreeWithPane(pane, true);
+ } else {
+ openDialog("chrome://communicator/content/pref/preferences.xul",
+ "PrefWindow",
+ "non-private,chrome,titlebar,dialog=no,resizable",
+ "paneLightning");
+ }
+ },
+ };
+
+ window.addEventListener("ViewChanged", lightningPrefs, false);
+ ]]></script>
+
+</overlay>
diff --git a/calendar/lightning/content/suite-overlay-preferences.xul b/calendar/lightning/content/suite-overlay-preferences.xul
new file mode 100644
index 000000000..976471527
--- /dev/null
+++ b/calendar/lightning/content/suite-overlay-preferences.xul
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://lightning/skin/lightning.css"?>
+
+<?xul-overlay href="chrome://calendar/content/preferences/general.xul"?>
+<?xul-overlay href="chrome://calendar/content/preferences/alarms.xul"?>
+<?xul-overlay href="chrome://calendar/content/preferences/categories.xul"?>
+<?xul-overlay href="chrome://calendar/content/preferences/views.xul"?>
+
+<!DOCTYPE overlay [
+ <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd">
+ %lightningDTD;
+ <!ENTITY % preferencesDTD SYSTEM "chrome://calendar/locale/preferences/preferences.dtd">
+ %preferencesDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <treechildren id="prefsPanelChildren">
+ <treeitem container="true"
+ id="lightningItem"
+ insertafter="mailnewsItem,navigatorItem"
+ label="&lightning.preferencesLabel;"
+ prefpane="paneLightning">
+ <treechildren id="lightningChildren">
+ <treeitem id="lightningAlarms"
+ label="&paneAlarms.title;"
+ prefpane="paneLightningAlarms"/>
+ <treeitem id="lightningCategories"
+ label="&paneCategories.title;"
+ prefpane="paneLightningCategories"/>
+ <treeitem id="lightningViews"
+ label="&paneViews.title;"
+ prefpane="paneLightningViews"/>
+ </treechildren>
+ </treeitem>
+ </treechildren>
+
+ <prefwindow id="prefDialog">
+ <prefpane id="paneLightning"
+ label="&lightning.preferencesLabel;"
+ onpaneload="gCalendarGeneralPane.init();">
+ <vbox id="calPreferencesBoxGeneral"/>
+ </prefpane>
+ <prefpane id="paneLightningAlarms"
+ label="&paneAlarms.title;"
+ onpaneload="gAlarmsPane.init();">
+ <vbox id="calPreferencesBoxAlarms"/>
+ </prefpane>
+ <prefpane id="paneLightningCategories"
+ label="&paneCategories.title;"
+ onpaneload="gCategoriesPane.init();">
+ <vbox id="calPreferencesBoxCategories"/>
+ </prefpane>
+ <prefpane id="paneLightningViews"
+ label="&paneViews.title;"
+ onpaneload="gViewsPane.init();">
+ <vbox id="calPreferencesBoxViews"/>
+ </prefpane>
+ </prefwindow>
+
+ <script type="application/javascript"
+ src="chrome://calendar/content/calUtils.js"/>
+</overlay>
diff --git a/calendar/lightning/content/suite-overlay-sidebar.js b/calendar/lightning/content/suite-overlay-sidebar.js
new file mode 100644
index 000000000..0ba313dd1
--- /dev/null
+++ b/calendar/lightning/content/suite-overlay-sidebar.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var ltnSuiteUtils = {
+
+ addStartupObserver: function() {
+ Services.obs.addObserver(this.startupObserver, "lightning-startup-done", false);
+ Services.obs.addObserver(this.startupObserver, "calendar-taskview-startup-done",
+ false);
+ },
+
+ startupObserver: {
+ observe: function(subject, topic, state) {
+ if (topic != "lightning-startup-done" &&
+ topic != "calendar-taskview-startup-done") {
+ return;
+ }
+
+ const ids = [
+ ["CustomizeTaskActionsToolbar", "task-actions-toolbox"],
+ ["CustomizeCalendarToolbar", "calendar-toolbox"],
+ ["CustomizeTaskToolbar", "task-toolbox"]
+ ];
+
+ ids.forEach(([itemID, toolboxID]) => {
+ let item = document.getElementById(itemID);
+ let toolbox = document.getElementById(toolboxID);
+ toolbox.customizeInit = function() {
+ item.setAttribute("disabled", "true");
+ toolboxCustomizeInit("mail-menubar");
+ };
+ toolbox.customizeDone = function(aToolboxChanged) {
+ item.removeAttribute("disabled");
+ toolboxCustomizeDone("mail-menubar", toolbox, aToolboxChanged);
+ };
+ toolbox.customizeChange = function(aEvent) {
+ toolboxCustomizeChange(toolbox, aEvent);
+ };
+ });
+ }
+ }
+};
+
+ltnSuiteUtils.addStartupObserver();
diff --git a/calendar/lightning/content/suite-overlay-sidebar.xul b/calendar/lightning/content/suite-overlay-sidebar.xul
new file mode 100644
index 000000000..4e00f7e48
--- /dev/null
+++ b/calendar/lightning/content/suite-overlay-sidebar.xul
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ - You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<overlay id="suiteSidebarOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript" src="chrome://lightning/content/suite-overlay-sidebar.js"/>
+
+ <key id="openLightningKey" removeelement="true"/>
+ <key id="openTasksKey" removeelement="true"/>
+ <key id="calendar-new-event-key" removeelement="true"/>
+ <key id="calendar-new-todo-key" removeelement="true"/>
+
+ <menuitem id="CustomizeTaskActionsToolbar"
+ oncommand="goCustomizeToolbar(document.getElementById('task-actions-toolbox'))"/>
+
+ <toolbox id="calendar-toolbox"
+ defaultlabelalign="end"
+ xpfe="false"/>
+ <toolbox id="task-toolbox"
+ defaultlabelalign="end"
+ xpfe="false"/>
+ <toolbox id="task-actions-toolbox"
+ defaultlabelalign="end"
+ xpfe="false"/>
+
+ <toolbar id="calendar-toolbar2"
+ defaultlabelalign="end"
+ context="toolbar-context-menu"/>
+ <toolbar id="task-toolbar2"
+ defaultlabelalign="end"
+ context="toolbar-context-menu"/>
+ <toolbar id="task-actions-toolbar"
+ context="toolbar-context-menu"/>
+
+ <toolbarset id="calendarToolbars" context="toolbar-context-menu"/>
+ <toolbarset id="taskToolbars" context="toolbar-context-menu"/>
+
+</overlay>
diff --git a/calendar/lightning/install.rdf b/calendar/lightning/install.rdf
new file mode 100644
index 000000000..fd34bf63c
--- /dev/null
+++ b/calendar/lightning/install.rdf
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+#filter substitution
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <!-- Target Application this extension can install into,
+ with minimum and maximum supported versions. -->
+ <em:targetApplication>
+ <Description>
+ <!-- Interlink -->
+ <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id>
+ <em:minVersion>@THUNDERBIRD_VERSION@</em:minVersion>
+ <em:maxVersion>@THUNDERBIRD_MAXVERSION@</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:id>@XPI_EM_ID@</em:id>
+ <em:name>Lightning</em:name>
+ <em:version>@LIGHTNING_VERSION@</em:version>
+ <em:description>Integrated Calendaring &amp; Scheduling for your Email client</em:description>
+ <em:creator>Mozilla Calendar Project</em:creator>
+ <em:homepageURL>https://www.mozilla.org/projects/calendar/</em:homepageURL>
+ <em:iconURL>chrome://calendar/skin/cal-icon32.png</em:iconURL>
+ <em:optionsURL>chrome://messenger/content/preferences/preferences.xul</em:optionsURL>
+ <em:targetPlatform>@TARGET_PLATFORM@</em:targetPlatform>
+ <em:unpack>true</em:unpack>
+ <em:strictCompatibility>true</em:strictCompatibility>
+ </Description>
+</RDF>
diff --git a/calendar/lightning/jar.mn b/calendar/lightning/jar.mn
new file mode 100644
index 000000000..4428ef5ba
--- /dev/null
+++ b/calendar/lightning/jar.mn
@@ -0,0 +1,111 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+lightning.jar:
+% override chrome://messagebody/skin/imip.css chrome://lightning/skin/imip.css
+% override chrome://messagebody/skin/calendar-event-dialog-attendees.png chrome://calendar-common/skin/calendar-event-dialog-attendees.png
+% overlay chrome://messenger/content/messenger.xul chrome://lightning/content/lightning-migration.xul
+% overlay chrome://messenger/content/messenger.xul chrome://lightning/content/lightning-item-panel.xul
+% overlay chrome://messenger/content/msgAccountCentral.xul chrome://lightning/content/messenger-overlay-accountCentral.xul
+% overlay chrome://messenger/content/messenger.xul chrome://lightning/content/messenger-overlay-sidebar.xul
+% overlay chrome://messenger/content/messageWindow.xul chrome://lightning/content/imip-bar-overlay.xul
+% overlay chrome://messenger/content/messageWindow.xul chrome://lightning/content/messenger-overlay-messageWindow.xul
+% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://lightning/content/imip-bar-overlay.xul
+% overlay chrome://communicator/content/pref/preferences.xul chrome://lightning/content/suite-overlay-preferences.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
+% overlay about:addons chrome://lightning/content/suite-overlay-addons.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
+% overlay chrome://mozapps/content/extensions/extensions.xul chrome://lightning/content/suite-overlay-addons.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
+% overlay chrome://messenger/content/preferences/preferences.xul chrome://lightning/content/messenger-overlay-preferences.xul
+% overlay about:preferences chrome://lightning/content/messenger-overlay-preferences.xul
+% overlay chrome://messenger/content/preferences/preferences.xul chrome://calendar/content/preferences/alarms.xul
+% overlay about:preferences chrome://calendar/content/preferences/alarms.xul
+% overlay chrome://messenger/content/preferences/preferences.xul chrome://calendar/content/preferences/categories.xul
+% overlay about:preferences chrome://calendar/content/preferences/categories.xul
+% overlay chrome://messenger/content/preferences/preferences.xul chrome://calendar/content/preferences/general.xul
+% overlay about:preferences chrome://calendar/content/preferences/general.xul
+% overlay chrome://messenger/content/preferences/preferences.xul chrome://calendar/content/preferences/views.xul
+% overlay about:preferences chrome://calendar/content/preferences/views.xul
+% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://calendar/content/calendar-unifinder.xul
+% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://calendar/content/calendar-unifinder-todo.xul
+% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://calendar/content/calendar-task-view.xul
+% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://calendar/content/today-pane.xul
+% overlay chrome://lightning/content/messenger-overlay-sidebar.xul chrome://lightning/content/suite-overlay-sidebar.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
+% overlay chrome://calendar/content/calendarCreation.xul chrome://lightning/content/lightning-calendar-creation.xul
+% overlay chrome://calendar/content/calendar-properties-dialog.xul chrome://lightning/content/lightning-calendar-properties.xul
+% override chrome://lightning/skin/accountCentral.css chrome://lightning-common/skin/suite-accountCentral.css application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
+% content lightning %content/lightning/
+ content/lightning/imip-bar.js (content/imip-bar.js)
+ content/lightning/imip-bar-overlay.xul (content/imip-bar-overlay.xul)
+ content/lightning/lightning-calendar-creation.xul (content/lightning-calendar-creation.xul)
+ content/lightning/lightning-calendar-creation.js (content/lightning-calendar-creation.js)
+ content/lightning/lightning-calendar-properties.xul (content/lightning-calendar-properties.xul)
+ content/lightning/lightning-calendar-properties.js (content/lightning-calendar-properties.js)
+ content/lightning/lightning-invitation.xhtml (content/lightning-invitation.xhtml)
+ content/lightning/lightning-menus.xul (content/lightning-menus.xul)
+ content/lightning/lightning-migration.xul (content/lightning-migration.xul)
+* content/lightning/lightning-toolbar.xul (content/lightning-toolbar.xul)
+ content/lightning/lightning-utils.js (content/lightning-utils.js)
+ content/lightning/lightning-widgets.css (content/lightning-widgets.css)
+ content/lightning/lightning-widgets.xml (content/lightning-widgets.xml)
+ content/lightning/messenger-overlay-accountCentral.xul (content/messenger-overlay-accountCentral.xul)
+ content/lightning/messenger-overlay-messageWindow.xul (content/messenger-overlay-messageWindow.xul)
+ content/lightning/messenger-overlay-sidebar.js (content/messenger-overlay-sidebar.js)
+ content/lightning/messenger-overlay-sidebar.xul (content/messenger-overlay-sidebar.xul)
+ content/lightning/messenger-overlay-preferences.js (content/messenger-overlay-preferences.js)
+ content/lightning/messenger-overlay-preferences.xul (content/messenger-overlay-preferences.xul)
+ content/lightning/suite-overlay-addons.xul (content/suite-overlay-addons.xul)
+ content/lightning/suite-overlay-preferences.xul (content/suite-overlay-preferences.xul)
+ content/lightning/suite-overlay-sidebar.js (content/suite-overlay-sidebar.js)
+ content/lightning/suite-overlay-sidebar.xul (content/suite-overlay-sidebar.xul)
+ content/lightning/lightning-item-toolbar.xul (content/lightning-item-toolbar.xul)
+* content/lightning/lightning-item-panel.xul (content/lightning-item-panel.xul)
+ content/lightning/lightning-item-panel.js (content/lightning-item-panel.js)
+ content/lightning/lightning-item-iframe.xul (content/lightning-item-iframe.xul)
+ content/lightning/lightning-item-iframe.js (content/lightning-item-iframe.js)
+ content/lightning/html-item-editing/lightning-item-iframe.html (content/html-item-editing/lightning-item-iframe.html)
+ content/lightning/html-item-editing/react-code.js (content/html-item-editing/react-code.js)
+% skin lightning classic/1.0 chrome/skin/linux/lightning/
+% skin lightning classic/1.0 chrome/skin/windows/lightning/ os=WINNT
+% skin lightning-common classic/1.0 chrome/skin/lightning-common/
+% style chrome://global/content/customizeToolbar.xul chrome://lightning/skin/lightning-toolbar.css
+% style chrome://calendar/content/calendar-event-dialog.xul chrome://communicator/skin/communicator.css application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
+% style chrome://global/content/customizeToolbar.xul chrome://lightning-common/skin/lightning.css application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
+% style chrome://global/content/customizeToolbar.xul chrome://calendar-common/skin/dialogs/calendar-event-dialog.css application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
+ ../skin/lightning-common/mode-switch-icons.png (themes/common/images/mode-switch-icons.png)
+ ../skin/lightning-common/suite-accountCentral.css (themes/common/suite-accountCentral.css)
+
+# Linux theme files
+ ../skin/linux/lightning/accountCentral.css (themes/linux/accountCentral.css)
+ ../skin/linux/lightning/imip.css (themes/linux/imip.css)
+ ../skin/linux/lightning/lightning.css (themes/linux/lightning.css)
+ ../skin/linux/lightning/lightning-toolbar.css (themes/linux/lightning-toolbar.css)
+ ../skin/linux/lightning/lightning-widgets.css (themes/linux/lightning-widgets.css)
+
+# Windows theme files
+ ../skin/windows/lightning/accountCentral.css (themes/windows/accountCentral.css)
+ ../skin/windows/lightning/imip.css (themes/windows/imip.css)
+ ../skin/windows/lightning/imip.png (themes/windows/imip.png)
+ ../skin/windows/lightning/lightning.css (themes/windows/lightning.css)
+ ../skin/windows/lightning/lightning-toolbar.css (themes/windows/lightning-toolbar.css)
+ ../skin/windows/lightning/lightning-widgets.css (themes/windows/lightning-widgets.css)
+ ../skin/windows/lightning/imip-aero.png (themes/windows/images/imip-aero.png)
+ ../skin/windows/lightning/mode-switch-icons-aero.png (themes/windows/images/mode-switch-icons-aero.png)
+ ../skin/windows/lightning/mode-switch-icons-inverted.png (themes/windows/images/mode-switch-icons-inverted.png)
+
+
+calendar.jar:
+ content/calendar/calendarCreation.xul (/calendar/resources/content/calendarCreation.xul)
+ content/calendar/calendarCreation.js (/calendar/resources/content/calendarCreation.js)
+ content/calendar/datetimepickers/datetimepickers.css (/calendar/resources/content/datetimepickers/datetimepickers.css)
+ content/calendar/datetimepickers/datetimepickers.xml (/calendar/resources/content/datetimepickers/datetimepickers.xml)
+ content/calendar/mouseoverPreviews.js (/calendar/resources/content/mouseoverPreviews.js)
+ content/calendar/publish.js (/calendar/resources/content/publish.js)
+ content/calendar/publishDialog.js (/calendar/resources/content/publishDialog.js)
+ content/calendar/publishDialog.xul (/calendar/resources/content/publishDialog.xul)
+ content/calendar/sound.wav (/calendar/resources/content/sound.wav)
+ ../skin/lightning-common/datetimepickers.css (/calendar/resources/skin/datetimepickers.css)
+ ../skin/lightning-common/dialogOverlay.css (/calendar/resources/skin/dialogOverlay.css)
+ ../skin/lightning-common/imip.css (themes/common/imip.css)
+ ../skin/lightning-common/lightning.css (themes/common/lightning.css)
+ ../skin/lightning-common/html-item-editing.css (themes/common/html-item-editing.css)
diff --git a/calendar/lightning/lightning-packager.mk b/calendar/lightning/lightning-packager.mk
new file mode 100644
index 000000000..8e36ce1b7
--- /dev/null
+++ b/calendar/lightning/lightning-packager.mk
@@ -0,0 +1,196 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# NOTE: The packager is not only used in calendar/lightning but should be
+# general enough to be able to repackage other sub-extensions like
+# calendar/providers/gdata. This means no lightning-specific files, no version
+# numbers directly from lightning and be careful with relative paths.
+
+# This packager can be used to repackage extensions. To use it, set the
+# following variables in your Makefile, then include this file.
+# XPI_NAME = lightning # The extension path name
+# XPI_PKGNAME = lightning-2.2.en-US.mac # The extension package name
+# XPI_VERSION = 2.2 # The extension version
+#
+# The following variables are optional:
+# XPI_NO_UNIVERSAL = 1 # If set, no universal path is used on mac
+#
+# For the upload target to work, you also need to set:
+# LIGHTNING_VERSION = 2.2 # Will be used to replace the Thunderbird version
+# # in POST_UPLOAD_CMD
+
+include $(MOZILLA_SRCDIR)/system/installer/package-name.mk
+
+# Set the univeral path only if we are building a univeral binary and it was
+# not restricted by the calling makefile
+ifeq ($(UNIVERSAL_BINARY)|$(XPI_NO_UNIVERSAL),1|)
+UNIVERSAL_PATH=universal/
+else
+UNIVERSAL_PATH=
+endif
+
+XPI_STAGE_PATH = $(DIST)/$(UNIVERSAL_PATH)xpi-stage
+_ABS_XPI_STAGE_PATH = $(ABS_DIST)/$(UNIVERSAL_PATH)xpi-stage
+ENUS_PKGNAME=$(subst .$(AB_CD).,.en-US.,$(XPI_PKGNAME))
+XPI_ZIP_IN=$(_ABS_XPI_STAGE_PATH)/$(ENUS_PKGNAME).xpi
+
+
+# This variable is to allow the wget-en-US target to know which ftp server to download from
+ifndef EN_US_BINARY_URL
+ifdef DOWNLOAD_HOST
+# If this url is missing, and DOWNLOAD_HOST is defined its probably the release
+# run where we can't influence the download location. Fake it from the env vars
+# we have
+BUILD_NR=$(shell echo $(POST_UPLOAD_CMD) | sed -n -e 's/.*-n \([0-9]*\).*/\1/p')
+CANDIDATE_NR=$(if $(LIGHTNING_VERSION),$(LIGHTNING_VERSION),$(XPI_VERSION))
+EN_US_BINARY_URL=http://$(DOWNLOAD_HOST)/pub/calendar/lightning/candidates/$(CANDIDATE_NR)-candidates/build$(BUILD_NR)/$(MOZ_PKG_PLATFORM)
+endif
+endif
+
+# Check if EN_US_BINARY_URL has finally been set
+ifdef EN_US_BINARY_URL
+# If so, we are expected to unpack when the language pack is created
+ensure-stage-dir: wget-en-US unpack
+else
+# If not, use the existing lightning from xpi-stage, or warn that the var is not set.
+ensure-stage-dir:
+ifeq (,$(wildcard $(XPI_STAGE_PATH)/$(XPI_NAME)/))
+ $(error You must set EN_US_BINARY_URL)
+endif
+endif
+
+$(XPI_STAGE_PATH):
+ mkdir -p $@
+
+# Target Directory used for the l10n files
+L10N_TARGET = $(XPI_STAGE_PATH)/$(XPI_NAME)-$(AB_CD)
+
+# Short name of the OS used in shipped-locales file. There are no
+# special cases, so assume linux for everything.
+SHORTOS = linux
+
+# function print_ltnconfig(section,configname)
+print_ltnconfig = $(shell $(PYTHON) $(MOZILLA_SRCDIR)/config/printconfigsetting.py $(XPI_STAGE_PATH)/$(XPI_NAME)/app.ini $1 $2)
+
+wget-en-US:
+ifeq (thunderbird,$(MOZ_APP_NAME))
+FINAL_BINARY_URL = $(subst thunderbird,calendar/lightning,$(EN_US_BINARY_URL))
+else
+FINAL_BINARY_URL = $(subst seamonkey,calendar/lightning,$(subst latest-comm-central-trunk,latest-comm-central,$(EN_US_BINARY_URL)))
+endif
+wget-en-US: $(XPI_STAGE_PATH)
+ (cd $(XPI_STAGE_PATH) && $(WGET) -nv -N $(FINAL_BINARY_URL)/$(ENUS_PKGNAME).xpi)
+ @echo "Downloaded $(FINAL_BINARY_URL)/$(ENUS_PKGNAME) to $(XPI_ZIP_IN)"
+
+
+# We're unpacking directly into FINAL_TARGET, this keeps code to do manual
+# repacks cleaner.
+unpack: $(XPI_ZIP_IN)
+ if test -d $(XPI_STAGE_PATH)/$(XPI_NAME); then \
+ $(RM) -r -v $(XPI_STAGE_PATH)/$(XPI_NAME); \
+ fi
+ $(NSINSTALL) -D $(XPI_STAGE_PATH)/$(XPI_NAME)
+ cd $(XPI_STAGE_PATH)/$(XPI_NAME) && $(UNZIP) $(XPI_ZIP_IN)
+ @echo done unpacking
+
+# Nothing to package for en-US, its just the usual english xpi
+langpack-en-US:
+ @echo "Skipping $@ as en-US is the default"
+
+merge-%:
+ifdef LOCALE_MERGEDIR
+ $(RM) -rf $(LOCALE_MERGEDIR)/calendar
+ $(MOZILLA_SRCDIR)/mach compare-locales \
+ --merge-dir $(LOCALE_MERGEDIR) \
+ --l10n-ini $(topsrcdir)/calendar/locales/l10n.ini \
+ $*
+
+ # This file requires a bugfix with string changes, see bug 1154448
+ [ -f $(L10NBASEDIR)/$*/calendar/chrome/calendar/calendar-extract.properties ] && \
+ $(RM) $(LOCALE_MERGEDIR)/calendar/chrome/calendar/calendar-extract.properties \
+ || true
+else
+ @echo "Not merging Lightning locales due to missing LOCALE_MERGEDIR"
+endif
+
+# Calling these targets with prerequisites causes the libs and subsequent
+# targets to be switched in order due to some make voodoo. Therefore we call
+# the targets explicitly, which seems to work better.
+langpack-%: L10N_XPI_NAME=$(XPI_NAME)-$*
+langpack-%: L10N_XPI_PKGNAME=$(subst $(AB_CD),$*,$(XPI_PKGNAME))
+langpack-%: AB_CD=$*
+langpack-%: ensure-stage-dir
+ $(MAKE) L10N_XPI_NAME=$(L10N_XPI_NAME) L10N_XPI_PKGNAME=$(L10N_XPI_PKGNAME) AB_CD=$(AB_CD) \
+ recreate-platformini repack-stage repack-process-extrafiles libs-$(AB_CD)
+ @echo "Done packaging $(L10N_XPI_PKGNAME).xpi"
+
+clobber-%: AB_CD=$*
+clobber-%:
+ $(RM) -r $(L10N_TARGET)
+
+repackage-zip-%:
+ @echo "Already repackaged zip for $* in langpack step"
+
+repack-stage:
+ @echo "Repackaging $(XPI_PKGNAME) locale for Language $(AB_CD)"
+ $(RM) -rf $(L10N_TARGET)
+ cp -R $(XPI_STAGE_PATH)/$(XPI_NAME) $(L10N_TARGET)
+ grep -v '^locale [a-z\-]\+ en-US' $(L10N_TARGET)/chrome.manifest > $(L10N_TARGET)/chrome.manifest~ && \
+ mv $(L10N_TARGET)/chrome.manifest~ $(L10N_TARGET)/chrome.manifest
+ find $(abspath $(L10N_TARGET)) -name '*en-US*' -print0 | xargs -0 rm -rf
+
+
+# Actual locale packaging targets. If L10N_XPI_NAME is set, then use it.
+# Otherwise keep the original XPI_NAME
+# Overriding the final target is a bit of a hack for universal builds
+# so that we can ensure we get the right xpi that gets repacked.
+libs-%: FINAL_XPI_NAME=$(if $(L10N_XPI_NAME),$(L10N_XPI_NAME),$(XPI_NAME))
+libs-%: FINAL_XPI_PKGNAME=$(if $(L10N_XPI_PKGNAME),$(L10N_XPI_PKGNAME),$(XPI_PKGNAME))
+libs-%:
+ $(MAKE) -C locales libs AB_CD=$* FINAL_TARGET=$(ABS_DIST)/$(UNIVERSAL_PATH)xpi-stage/$(FINAL_XPI_NAME) \
+ XPI_NAME=$(FINAL_XPI_NAME) XPI_PKGNAME=$(FINAL_XPI_PKGNAME) USE_EXTENSION_MANIFEST=1
+ $(MAKE) -C locales tools AB_CD=$* FINAL_TARGET=$(ABS_DIST)/$(UNIVERSAL_PATH)xpi-stage/$(FINAL_XPI_NAME) \
+ XPI_NAME=$(FINAL_XPI_NAME) XPI_PKGNAME=$(FINAL_XPI_PKGNAME) USE_EXTENSION_MANIFEST=1
+
+# The calling makefile might need to process some extra files. Provide an empty
+# rule to overwrite
+repack-process-extrafiles:
+
+# When repackaging Lightning from the builder, platform.ini is not yet created.
+# Recreate it from the app.ini bundled with the downloaded xpi.
+$(DIST)/bin/platform.ini:
+ mkdir -p $(@D)
+ echo "[Build]" >> $(DIST)/bin/platform.ini
+ echo "Milestone=$(call print_ltnconfig,Gecko,MaxVersion)" >> $(DIST)/bin/platform.ini
+ echo "SourceStamp=$(call print_ltnconfig,Build,SourceStamp)" >> $(DIST)/bin/platform.ini
+ echo "SourceRepository=$(call print_ltnconfig,Build,SourceRepository)" >> $(DIST)/bin/platform.ini
+ echo "BuildID=$(call print_ltnconfig,App,BuildID)" >> $(DIST)/bin/platform.ini
+
+recreate-platformini: $(DIST)/bin/platform.ini
+
+
+# Lightning uses Thunderbird's build machinery, so we need to hack the post
+# upload command to use Lightning's directories and version.
+upload: upload-$(AB_CD)
+upload-%: LTN_UPLOAD_CMD := $(patsubst $(THUNDERBIRD_VERSION)%,$(LIGHTNING_VERSION),$(subst thunderbird,calendar/lightning,$(POST_UPLOAD_CMD)))
+upload-%: stage_upload
+ POST_UPLOAD_CMD="$(LTN_UPLOAD_CMD)" \
+ $(PYTHON) $(MOZILLA_DIR)/build/upload.py --base-path $(DIST) \
+ --properties-file $(DIST)/$(XPI_NAME)_build_properties.json \
+ "$(DIST)/$(MOZ_PKG_PLATFORM)/$(XPI_PKGNAME).xpi"
+
+stage_upload:
+ $(NSINSTALL) -D $(DIST)/$(MOZ_PKG_PLATFORM)
+ $(call install_cmd,$(IFLAGS1) $(XPI_STAGE_PATH)/$(XPI_PKGNAME).xpi $(DIST)/$(MOZ_PKG_PLATFORM))
+
+ifdef XPI_INSTALL_EXTENSION
+ifndef XPI_NAME
+$(error XPI_NAME must be set for XPI_INSTALL_EXTENSION)
+endif
+tools::
+ $(RM) -r '$(DIST)/bin$(DIST_SUBDIR:%=/%)/extensions/$(XPI_INSTALL_EXTENSION)'
+ $(NSINSTALL) -D '$(DIST)/bin$(DIST_SUBDIR:%=/%)/extensions/$(XPI_INSTALL_EXTENSION)'
+ $(call copy_dir,$(FINAL_TARGET),$(DIST)/bin$(DIST_SUBDIR:%=/%)/extensions/$(XPI_INSTALL_EXTENSION))
+
+endif
diff --git a/calendar/lightning/lightning-tests.mk b/calendar/lightning/lightning-tests.mk
new file mode 100644
index 000000000..334ca6769
--- /dev/null
+++ b/calendar/lightning/lightning-tests.mk
@@ -0,0 +1,23 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+PKG_STAGE = $(DIST)/test-stage
+
+# This is the target that should be called externally
+stage-package: stage-extension stage-mozmill
+
+# stage the extension, avoiding per-platform differences so that the mac unify
+# target works.
+stage-extension:
+ $(NSINSTALL) -D $(PKG_STAGE)/extensions/$(XPI_EM_ID)
+ (cd $(FINAL_TARGET) && tar $(TAR_CREATE_FLAGS) - *) | (cd $(PKG_STAGE)/extensions/$(XPI_EM_ID) && tar -xf -)
+ grep -v em:targetPlatform $(FINAL_TARGET)/install.rdf > $(PKG_STAGE)/extensions/$(XPI_EM_ID)/install.rdf
+
+# stage mozmill tests and shared modules. Cross your fingers that there are no
+# name conflicts between calendar/ and mail/
+stage-mozmill:
+ $(NSINSTALL) -D $(PKG_STAGE)/mozmill/shared-modules
+ (cd $(topsrcdir)/calendar/test/mozmill && tar $(TAR_CREATE_FLAGS) - `cat $(topsrcdir)/calendar/test/mozmill/mozmilltests.list`) | (cd $(PKG_STAGE)/mozmill && tar -xf -)
+ (cd $(topsrcdir)/calendar/test/mozmill/shared-modules && tar $(TAR_CREATE_FLAGS) - *) | (cd $(PKG_STAGE)/mozmill/shared-modules && tar -xf -)
+ $(call py_action,buildlist,$(PKG_STAGE)/mozmill/mozmilltests.list $(shell cat $(topsrcdir)/calendar/test/mozmill/mozmilltests.list))
diff --git a/calendar/lightning/locales/Makefile.in b/calendar/lightning/locales/Makefile.in
new file mode 100644
index 000000000..1ef5dd430
--- /dev/null
+++ b/calendar/lightning/locales/Makefile.in
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Setting this to calendar/locales sets up LOCALE_SRCDIR to the correct locale
+# directory
+relativesrcdir = calendar/locales
+
+DEFINES += -DTHEME=$(THEME) \
+ -DLOCALE_SRCDIR=$(LOCALE_SRCDIR) \
+ $(NULL)
diff --git a/calendar/lightning/locales/jar.mn b/calendar/lightning/locales/jar.mn
new file mode 100644
index 000000000..aabcdcc74
--- /dev/null
+++ b/calendar/lightning/locales/jar.mn
@@ -0,0 +1,11 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+lightning-@AB_CD@.jar:
+% locale lightning @AB_CD@ %locale/@AB_CD@/lightning/
+ locale/@AB_CD@/lightning/lightning.dtd (%chrome/lightning/lightning.dtd)
+ locale/@AB_CD@/lightning/lightning-toolbar.dtd (%chrome/lightning/lightning-toolbar.dtd)
+ locale/@AB_CD@/lightning/lightning.properties (%chrome/lightning/lightning.properties)
diff --git a/calendar/lightning/locales/moz.build b/calendar/lightning/locales/moz.build
new file mode 100644
index 000000000..7146412a6
--- /dev/null
+++ b/calendar/lightning/locales/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ['../../locales']
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/calendar/lightning/modules/ltnInvitationUtils.jsm b/calendar/lightning/modules/ltnInvitationUtils.jsm
new file mode 100644
index 000000000..3b50f25c4
--- /dev/null
+++ b/calendar/lightning/modules/ltnInvitationUtils.jsm
@@ -0,0 +1,588 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource://calendar/modules/calXMLUtils.jsm");
+Components.utils.import("resource://calendar/modules/calRecurrenceUtils.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://calendar/modules/ltnUtils.jsm");
+Components.utils.import("resource:///modules/mailServices.js");
+
+this.EXPORTED_SYMBOLS = ["ltn"]; // even though it's defined in ltnUtils.jsm, import needs this
+ltn.invitation = {
+ /**
+ * Returns a header title for an ITIP item depending on the response method
+ * @param {calItipItem} aItipItem the itip item to check
+ * @return {String} the header title
+ */
+ getItipHeader: function(aItipItem) {
+ let header;
+
+ if (aItipItem) {
+ let item = aItipItem.getItemList({})[0];
+ let summary = item.getProperty("SUMMARY") || "";
+ let organizer = item.organizer;
+ let organizerString = (organizer) ?
+ (organizer.commonName || organizer.toString()) : "";
+
+ switch (aItipItem.responseMethod) {
+ case "REQUEST":
+ header = ltn.getString("lightning",
+ "itipRequestBody",
+ [organizerString, summary]);
+ break;
+ case "CANCEL":
+ header = ltn.getString("lightning",
+ "itipCancelBody",
+ [organizerString, summary]);
+ break;
+ case "COUNTER":
+ // falls through
+ case "REPLY":
+ let attendees = item.getAttendees({});
+ let sender = cal.getAttendeesBySender(attendees, aItipItem.sender);
+ if (sender.length == 1) {
+ if (aItipItem.responseMethod == "COUNTER") {
+ header = cal.calGetString("lightning",
+ "itipCounterBody",
+ [sender[0].toString(), summary],
+ "lightning");
+ } else {
+ let statusString = (sender[0].participationStatus == "DECLINED" ?
+ "itipReplyBodyDecline" : "itipReplyBodyAccept");
+ header = cal.calGetString("lightning",
+ statusString,
+ [sender[0].toString()],
+ "lightning");
+ }
+ } else {
+ header = "";
+ }
+ break;
+ case "DECLINECOUNTER":
+ header = ltn.getString("lightning",
+ "itipDeclineCounterBody",
+ [organizerString, summary]);
+ break;
+ }
+ }
+
+ if (!header) {
+ header = ltn.getString("lightning", "imipHtml.header", null);
+ }
+
+ return header;
+ },
+
+ /**
+ * Returns the html representation of the event as a DOM document.
+ *
+ * @param {calIItemBase} aEvent The event to parse into html.
+ * @param {calItipItem} aItipItem The itip item, which containes aEvent.
+ * @return {DOM} The html representation of aEvent.
+ */
+ createInvitationOverlay: function(aEvent, aItipItem) {
+ // Creates HTML using the Node strings in the properties file
+ let doc = cal.xml.parseFile("chrome://lightning/content/lightning-invitation.xhtml");
+ let formatter = cal.getDateFormatter();
+
+ let linkConverter = Components.classes["@mozilla.org/txttohtmlconv;1"]
+ .getService(Components.interfaces.mozITXTToHTMLConv);
+
+ let field = function(aField, aContentText, aConvert) {
+ let descr = doc.getElementById("imipHtml-" + aField + "-descr");
+ if (descr) {
+ let labelText = ltn.getString("lightning", "imipHtml." + aField, null);
+ descr.textContent = labelText;
+ }
+
+ if (aContentText) {
+ let content = doc.getElementById("imipHtml-" + aField + "-content");
+ doc.getElementById("imipHtml-" + aField + "-row").hidden = false;
+ if (aConvert) {
+ // we convert special characters first to not mix up html conversion
+ let mode = Components.interfaces.mozITXTToHTMLConv.kEntities;
+ let contentText = linkConverter.scanTXT(aContentText, mode);
+ try {
+ // kGlyphSubstitution may lead to unexpected results when used in scanHTML
+ mode = Components.interfaces.mozITXTToHTMLConv.kStructPhrase +
+ Components.interfaces.mozITXTToHTMLConv.kGlyphSubstitution +
+ Components.interfaces.mozITXTToHTMLConv.kURLs;
+ content.innerHTML = linkConverter.scanHTML(contentText, mode);
+ } catch (e) {
+ mode = Components.interfaces.mozITXTToHTMLConv.kStructPhrase +
+ Components.interfaces.mozITXTToHTMLConv.kURLs;
+ content.innerHTML = linkConverter.scanHTML(contentText, mode);
+ }
+ } else {
+ content.textContent = aContentText;
+ }
+ }
+ };
+
+ // Simple fields
+ let headerDescr = doc.getElementById("imipHtml-header-descr");
+ if (headerDescr) {
+ headerDescr.textContent = ltn.invitation.getItipHeader(aItipItem);
+ }
+
+ field("summary", aEvent.title, true);
+ field("location", aEvent.getProperty("LOCATION"), true);
+
+ let dateString = formatter.formatItemInterval(aEvent);
+
+ if (aEvent.recurrenceInfo) {
+ let kDefaultTimezone = cal.calendarDefaultTimezone();
+ let startDate = aEvent.startDate;
+ let endDate = aEvent.endDate;
+ startDate = startDate ? startDate.getInTimezone(kDefaultTimezone) : null;
+ endDate = endDate ? endDate.getInTimezone(kDefaultTimezone) : null;
+ let repeatString = recurrenceRule2String(aEvent.recurrenceInfo, startDate,
+ endDate, startDate.isDate);
+ if (repeatString) {
+ dateString = repeatString;
+ }
+
+ let formattedExDates = [];
+ let modifiedOccurrences = [];
+
+ let dateComptor = function(a, b) {
+ return a.startDate.compare(b.startDate);
+ };
+
+ // Show removed instances
+ for (let exc of aEvent.recurrenceInfo.getRecurrenceItems({})) {
+ if (exc instanceof Components.interfaces.calIRecurrenceDate) {
+ if (exc.isNegative) {
+ // This is an EXDATE
+ formattedExDates.push(formatter.formatDateTime(exc.date));
+ } else {
+ // This is an RDATE, close enough to a modified occurrence
+ let excItem = aEvent.recurrenceInfo.getOccurrenceFor(exc.date);
+ cal.binaryInsert(modifiedOccurrences, excItem, dateComptor, true);
+ }
+ }
+ }
+ if (formattedExDates.length > 0) {
+ field("canceledOccurrences", formattedExDates.join("\n"));
+ }
+
+ // Show modified occurrences
+ for (let recurrenceId of aEvent.recurrenceInfo.getExceptionIds({})) {
+ let exc = aEvent.recurrenceInfo.getExceptionFor(recurrenceId);
+ let excLocation = exc.getProperty("LOCATION");
+
+ // Only show modified occurrence if start, duration or location
+ // has changed.
+ if (exc.startDate.compare(exc.recurrenceId) != 0 ||
+ exc.duration.compare(aEvent.duration) != 0 ||
+ excLocation != aEvent.getProperty("LOCATION")) {
+ cal.binaryInsert(modifiedOccurrences, exc, dateComptor, true);
+ }
+ }
+
+ let stringifyOcc = function(occ) {
+ let formattedExc = formatter.formatItemInterval(occ);
+ let occLocation = occ.getProperty("LOCATION");
+ if (occLocation != aEvent.getProperty("LOCATION")) {
+ let location = ltn.getString("lightning", "imipHtml.newLocation", [occLocation]);
+ formattedExc += " (" + location + ")";
+ }
+ return formattedExc;
+ };
+
+ if (modifiedOccurrences.length > 0) {
+ field("modifiedOccurrences", modifiedOccurrences.map(stringifyOcc).join("\n"));
+ }
+ }
+
+ field("when", dateString);
+ field("comment", aEvent.getProperty("COMMENT"), true);
+
+ // DESCRIPTION field
+ let eventDescription = (aEvent.getProperty("DESCRIPTION") || "")
+ /* Remove the useless "Outlookism" squiggle. */
+ .replace("*~*~*~*~*~*~*~*~*~*", "");
+ field("description", eventDescription, true);
+
+ // URL
+ field("url", aEvent.getProperty("URL"), true);
+
+ // ATTACH - we only display URI but no BINARY type attachments here
+ let links = [];
+ let attachments = aEvent.getAttachments({});
+ for (let attachment of attachments) {
+ if (attachment.uri) {
+ links.push(attachment.uri.spec);
+ }
+ }
+ field("attachments", links.join("<br>"), true);
+
+ // ATTENDEE and ORGANIZER fields
+ let attendees = aEvent.getAttendees({});
+ let attendeeTemplate = doc.getElementById("attendee-template");
+ let attendeeTable = doc.getElementById("attendee-table");
+ let organizerTable = doc.getElementById("organizer-table");
+ doc.getElementById("imipHtml-attendees-row").hidden = (attendees.length < 1);
+ doc.getElementById("imipHtml-organizer-row").hidden = !aEvent.organizer;
+
+ let setupAttendee = function(aAttendee) {
+ let row = attendeeTemplate.cloneNode(true);
+ row.removeAttribute("id");
+ row.removeAttribute("hidden");
+
+ // resolve delegatees/delegators to display also the CN
+ let del = cal.resolveDelegation(aAttendee, attendees);
+ if (del.delegators != "") {
+ del.delegators = " " + ltn.getString("lightning", "imipHtml.attendeeDelegatedFrom",
+ [del.delegators]);
+ }
+
+ // display itip icon
+ let role = aAttendee.role || "REQ-PARTICIPANT";
+ let partstat = aAttendee.participationStatus || "NEEDS-ACTION";
+ let userType = aAttendee.userType || "INDIVIDUAL";
+ let itipIcon = row.getElementsByClassName("itip-icon")[0];
+ itipIcon.setAttribute("role", role);
+ itipIcon.setAttribute("usertype", userType);
+ itipIcon.setAttribute("partstat", partstat);
+ let attName = aAttendee.commonName && aAttendee.commonName.length
+ ? aAttendee.commonName : aAttendee.toString();
+ let userTypeString = ltn.getString("lightning", "imipHtml.attendeeUserType2." + userType,
+ [aAttendee.toString()]);
+ let roleString = ltn.getString("lightning", "imipHtml.attendeeRole2." + role,
+ [userTypeString]);
+ let partstatString = ltn.getString("lightning", "imipHtml.attendeePartStat2." + partstat,
+ [attName, del.delegatees]);
+ let itipTooltip = ltn.getString("lightning", "imipHtml.attendee.combined",
+ [roleString, partstatString]);
+ row.setAttribute("title", itipTooltip);
+ // display attendee
+ row.getElementsByClassName("attendee-name")[0].textContent = aAttendee.toString() +
+ del.delegators;
+ return row;
+ };
+
+ // Fill rows for attendees and organizer
+ field("attendees");
+ for (let attendee of attendees) {
+ attendeeTable.appendChild(setupAttendee(attendee));
+ }
+
+ field("organizer");
+ if (aEvent.organizer) {
+ organizerTable.appendChild(setupAttendee(aEvent.organizer));
+ }
+
+ return doc;
+ },
+
+ /**
+ * Expects and return a serialized DOM - use cal.xml.serializeDOM(aDOM)
+ * @param {String} aOldDoc serialized DOM of the the old document
+ * @param {String} aNewDoc serialized DOM of the the new document
+ * @param {String} aIgnoreId attendee id to ignore, usually the organizer
+ * @return {String} updated serialized DOM of the new document
+ */
+ compareInvitationOverlay: function(aOldDoc, aNewDoc, aIgnoreId) {
+ /**
+ * Transforms text node content to formated child nodes. Decorations are defined in imip.css
+ * @param {Node} aToNode text node to change
+ * @param {String} aType use 'newline' for the same, 'added' or 'removed' for decoration
+ * @param {String} aText [optional]
+ * @param {Boolean} aClear [optional] for consecutive changes on the same node, set to false
+ */
+ function _content2Child(aToNode, aType, aText = "", aClear = true) {
+ let nodeDoc = aToNode.ownerDocument;
+ if (aClear && aToNode.hasChildNodes()) {
+ aToNode.removeChild(aToNode.firstChild);
+ }
+ let n = nodeDoc.createElement(aType.toLowerCase() == "newline" ? "br" : "span");
+ switch (aType) {
+ case "added":
+ case "modified":
+ case "removed":
+ n.className = aType;
+ if (Preferences.get("calendar.view.useSystemColors", false)) {
+ n.setAttribute("systemcolors", true);
+ }
+ break;
+ }
+ n.textContent = aText;
+ aToNode.appendChild(n);
+ }
+ /**
+ * Extracts attendees from the given document
+ * @param {Node} aDoc document to search in
+ * @param {String} aElement element name as used in _compareElement()
+ * @returns {Array} attendee nodes
+ */
+ function _getAttendees(aDoc, aElement) {
+ let attendees = [];
+ for (let att of aDoc.getElementsByClassName("attendee-name")) {
+ if (!att.parentNode.hidden &&
+ att.parentNode.parentNode.id == (aElement + "-table")) {
+ attendees[att.textContent] = att;
+ }
+ }
+ return attendees;
+ }
+ /**
+ * Compares both documents for elements related to the given name
+ * @param {String} aElement part of the element id within the html template
+ */
+ function _compareElement(aElement) {
+ let element = aElement == "attendee" ? aElement + "s" : aElement;
+ let oldRow = aOldDoc.getElementById("imipHtml-" + element + "-row");
+ let newRow = aNewDoc.getElementById("imipHtml-" + element + "-row");
+ let row = doc.getElementById("imipHtml-" + element + "-row");
+ let oldContent = aOldDoc.getElementById("imipHtml-" + aElement + "-content");
+ let content = doc.getElementById("imipHtml-" + aElement + "-content");
+
+ if (newRow.hidden && !oldRow.hidden) {
+ // element was removed
+ // we only need to check for simple elements here: attendee or organizer row
+ // cannot be removed
+ if (oldContent) {
+ _content2Child(content, "removed", oldContent.textContent);
+ row.hidden = false;
+ }
+ } else if (!newRow.hidden && oldRow.hidden) {
+ // the element was added
+ // we only need to check for simple elements here: attendee or organizer row
+ // must have been there before
+ if (content) {
+ _content2Child(content, "added", content.textContent);
+ }
+ } else if (!newRow.hidden && !oldRow.hidden) {
+ // the element may have been modified
+ if (content) {
+ if (content.textContent != oldContent.textContent) {
+ _content2Child(content, "added", content.textContent);
+ _content2Child(content, "newline", null, false);
+ _content2Child(content, "removed", oldContent.textContent, false);
+ }
+ } else {
+ content = doc.getElementById(aElement + "-table");
+ oldContent = aOldDoc.getElementById(aElement + "-table");
+ let excludeAddress = cal.removeMailTo(aIgnoreId);
+ if (content && oldContent && !content.isEqualNode(oldContent)) {
+ // extract attendees
+ let attendees = _getAttendees(doc, aElement);
+ let oldAttendees = _getAttendees(aOldDoc, aElement);
+ // decorate newly added attendees
+ for (let att of Object.keys(attendees)) {
+ if (!(att in oldAttendees)) {
+ _content2Child(attendees[att], "added", att);
+ }
+ }
+ for (let att of Object.keys(oldAttendees)) {
+ // if att is the user his/herself, who accepted an invitation he/she was
+ // not invited to, we exclude him/her from decoration
+ let notExcluded = excludeAddress == "" ||
+ !att.includes(excludeAddress);
+ // decorate removed attendees
+ if (!(att in attendees) && notExcluded) {
+ _content2Child(oldAttendees[att], "removed", att);
+ content.appendChild(oldAttendees[att].parentNode.cloneNode(true));
+ } else if ((att in attendees) && notExcluded) {
+ // highlight partstat, role or usertype changes
+ let oldAtts = oldAttendees[att].parentNode
+ .getElementsByClassName("itip-icon")[0]
+ .attributes;
+ let newAtts = attendees[att].parentNode
+ .getElementsByClassName("itip-icon")[0]
+ .attributes;
+ let hasChanged = function(name) {
+ return oldAtts.getNamedItem(name).value !=
+ newAtts.getNamedItem(name).value;
+ };
+ if (["role", "partstat", "usertype"].some(hasChanged)) {
+ _content2Child(attendees[att], "modified", att);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ aOldDoc = cal.xml.parseString(aOldDoc);
+ aNewDoc = cal.xml.parseString(aNewDoc);
+ let doc = aNewDoc.cloneNode(true);
+ // elements to consider for comparison
+ ["summary", "location", "when", "canceledOccurrences",
+ "modifiedOccurrences", "organizer", "attendee"].forEach(_compareElement);
+ return cal.xml.serializeDOM(doc);
+ },
+
+ /**
+ * Returns the header section for an invitation email.
+ * @param {String} aMessageId the message id to use for that email
+ * @param {nsIMsgIdentity} aIdentity the identity to use for that email
+ * @returns {String} the source code of the header section of the email
+ */
+ getHeaderSection: function(aMessageId, aIdentity, aToList, aSubject) {
+ let recipient = aIdentity.fullName + " <" + aIdentity.email + ">";
+ let from = aIdentity.fullName.length ? cal.validateRecipientList(recipient)
+ : aIdentity.email;
+ let header = "MIME-version: 1.0\r\n" +
+ (aIdentity.replyTo ? "Return-path: " +
+ ltn.invitation.encodeMimeHeader(aIdentity.replyTo, true) +
+ "\r\n" : "") +
+ "From: " + ltn.invitation.encodeMimeHeader(from, true) + "\r\n" +
+ (aIdentity.organization ? "Organization: " +
+ ltn.invitation.encodeMimeHeader(aIdentity.organization) +
+ "\r\n" : "") +
+ "Message-ID: " + aMessageId + "\r\n" +
+ "To: " + ltn.invitation.encodeMimeHeader(aToList, true) + "\r\n" +
+ "Date: " + ltn.invitation.getRfc5322FormattedDate() + "\r\n" +
+ "Subject: " + ltn.invitation
+ .encodeMimeHeader(aSubject.replace(/(\n|\r\n)/, "|")) + "\r\n";
+ let validRecipients;
+ if (aIdentity.doCc) {
+ validRecipients = cal.validateRecipientList(aIdentity.doCcList);
+ if (validRecipients != "") {
+ header += "Cc: " + ltn.invitation.encodeMimeHeader(validRecipients, true) + "\r\n";
+ }
+ }
+ if (aIdentity.doBcc) {
+ validRecipients = cal.validateRecipientList(aIdentity.doBccList);
+ if (validRecipients != "") {
+ header += "Bcc: " + ltn.invitation.encodeMimeHeader(validRecipients, true) + "\r\n";
+ }
+ }
+ return header;
+ },
+
+ /**
+ * Returns a datetime string according to section 3.3 of RfC5322
+ * @param {Date} [optional] Js Date object to format; if not provided current DateTime is used
+ * @return {String} Datetime string with a modified tz-offset notation compared to
+ * Date.toString() like "Fri, 20 Nov 2015 09:45:36 +0100"
+ */
+ getRfc5322FormattedDate: function(aDate = null) {
+ let date = aDate || new Date();
+ let str = date.toString()
+ .replace(/^(\w{3}) (\w{3}) (\d{2}) (\d{4}) ([0-9:]{8}) GMT([+-])(\d{4}).*$/,
+ "$1, $3 $2 $4 $5 $6$7");
+ // according to section 3.3 of RfC5322, +0000 should be used for defined timezones using
+ // UTC time, while -0000 should indicate a floating time instead
+ let timezone = cal.calendarDefaultTimezone();
+ if (timezone && timezone.isFloating) {
+ str.replace(/\+0000$/, "-0000");
+ }
+ return str;
+ },
+
+ /**
+ * Converts a given unicode text to utf-8 and normalizes line-breaks to \r\n
+ * @param {String} aText a unicode encoded string
+ * @return {String} the converted uft-8 encoded string
+ */
+ encodeUTF8: function(aText) {
+ return ltn.invitation.convertFromUnicode("UTF-8", aText).replace(/(\r\n)|\n/g, "\r\n");
+ },
+
+ /**
+ * Converts a given unicode text
+ * @param {String} aCharset target character set
+ * @param {String} aSrc unicode text to convert
+ * @return {String} the converted string
+ */
+ convertFromUnicode: function(aCharset, aSrc) {
+ let unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+ unicodeConverter.charset = aCharset;
+ return unicodeConverter.ConvertFromUnicode(aSrc);
+ },
+
+ /**
+ * Converts a header to a mime encoded header
+ * @param {String} aHeader a header to encode
+ * @param {boolean} aIsEmail if enabled, only the CN but not the email address gets
+ * converted - default value is false
+ * @return {String} the encoded string
+ */
+ encodeMimeHeader: function(aHeader, aIsEmail = false) {
+ let fieldNameLen = aHeader.indexOf(": ") + 2;
+ return MailServices.mimeConverter
+ .encodeMimePartIIStr_UTF8(aHeader,
+ aIsEmail,
+ "UTF-8",
+ fieldNameLen,
+ Components.interfaces
+ .nsIMimeConverter
+ .MIME_ENCODED_WORD_SIZE);
+ },
+
+ /**
+ * Parses a counterproposal to extract differences to the existing event
+ * @param {calIEvent|calITodo} aProposedItem The counterproposal
+ * @param {calIEvent|calITodo} aExistingItem The item to compare with
+ * @return {JSObject} Objcet of result and differences of parsing
+ * @return {String} JsObject.result.type Parsing result: OK|OLDVERSION|ERROR|NODIFF
+ * @return {String} JsObject.result.descr Parsing result description
+ * @return {Array} JsObject.differences Array of objects consisting of property, proposed
+ * and original properties.
+ * @return {String} JsObject.comment A comment of the attendee, if any
+ */
+ parseCounter: function(aProposedItem, aExistingItem) {
+ let isEvent = cal.isEvent(aProposedItem);
+ // atm we only support a subset of properties, for a full list see RfC 5546 section 3.2.7
+ let properties = ["SUMMARY", "LOCATION", "DTSTART", "DTEND", "COMMENT"];
+ if (!isEvent) {
+ cal.LOG("Parsing of counterproposals is currently only supported for events.");
+ properties = [];
+ }
+
+ let diff = [];
+ let status = { descr: "", type: "OK" };
+ // As required in https://tools.ietf.org/html/rfc5546#section-3.2.7 a valid counterproposal
+ // is referring to as existing UID and must include the same sequence number and organizer as
+ // the original request being countered
+ if (aProposedItem.id == aExistingItem.id &&
+ aProposedItem.organizer && aExistingItem.organizer &&
+ aProposedItem.organizer.id == aExistingItem.organizer.id) {
+ let proposedSequence = aProposedItem.getProperty("SEQUENCE") || 0;
+ let existingSequence = aExistingItem.getProperty("SEQUENCE") || 0;
+ if (existingSequence >= proposedSequence) {
+ if (existingSequence > proposedSequence) {
+ // in this case we prompt the organizer with the additional information that the
+ // received proposal refers to an outdated version of the event
+ status.descr = "This is a counterproposal to an already rescheduled event.";
+ status.type = "OUTDATED";
+ } else if (aProposedItem.stampTime.compare(aExistingItem.stampTime) == -1) {
+ // now this is the same sequence but the proposal is not based on the latest
+ // update of the event - updated events may have minor changes, while for major
+ // ones there has been a rescheduling
+ status.descr = "This is a counterproposal not based on the latest event update.";
+ status.type = "NOTLATESTUPDATE";
+ }
+ for (let prop of properties) {
+ let newValue = aProposedItem.getProperty(prop) || null;
+ let oldValue = aExistingItem.getProperty(prop) || null;
+ if ((["DTSTART", "DTEND"].includes(prop) && newValue.toString() != oldValue.toString()) ||
+ (!["DTSTART", "DTEND"].includes(prop) && newValue != oldValue)) {
+ diff.push({
+ property: prop,
+ proposed: newValue,
+ original: oldValue
+ });
+ }
+ }
+ } else {
+ status.descr = "Invalid sequence number in counterproposal.";
+ status.type = "ERROR";
+ }
+ } else {
+ status.descr = "Mismatch of uid or organizer in counterproposal.";
+ status.type = "ERROR";
+ }
+ if (status.type != "ERROR" && !diff.length) {
+ status.descr = "No difference in counterproposal detected.";
+ status.type = "NODIFF";
+ }
+ return { result: status, differences: diff };
+ }
+};
diff --git a/calendar/lightning/modules/ltnUtils.jsm b/calendar/lightning/modules/ltnUtils.jsm
new file mode 100644
index 000000000..22444aab9
--- /dev/null
+++ b/calendar/lightning/modules/ltnUtils.jsm
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* exported ltn */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["ltn"];
+var ltn = {
+ /**
+ * Gets the value of a string in a .properties file from the lightning bundle
+ *
+ * @param {String} aBundleName the name of the properties file. It is assumed that the
+ * file lives in chrome://lightning/locale/
+ * @param {String} aStringName the name of the string within the properties file
+ * @param {Array} aParams [optional] array of parameters to format the string
+ */
+ getString: function(aBundleName, aStringName, aParams) {
+ return cal.calGetString(aBundleName, aStringName, aParams, "lightning");
+ }
+};
diff --git a/calendar/lightning/modules/moz.build b/calendar/lightning/modules/moz.build
new file mode 100644
index 000000000..b7a3ae4f2
--- /dev/null
+++ b/calendar/lightning/modules/moz.build
@@ -0,0 +1,9 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES += [
+ 'ltnInvitationUtils.jsm',
+ 'ltnUtils.jsm',
+]
diff --git a/calendar/lightning/moz.build b/calendar/lightning/moz.build
new file mode 100644
index 000000000..c6f12d640
--- /dev/null
+++ b/calendar/lightning/moz.build
@@ -0,0 +1,53 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ '../base',
+ '../components',
+ '../providers/gdata',
+ 'components',
+ 'locales',
+ 'modules',
+]
+
+TEST_DIRS += ['../test']
+
+XPI_NAME = 'lightning'
+export('XPI_NAME')
+
+FINAL_TARGET_PP_FILES += [
+ 'app.ini',
+ 'install.rdf',
+]
+
+JAR_MANIFESTS += ['jar.mn']
+
+USE_EXTENSION_MANIFEST = True
+export('USE_EXTENSION_MANIFEST')
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
+ DEFINES['THEME'] = 'windows'
+else:
+ DEFINES['THEME'] = 'linux'
+
+JS_PREFERENCE_PP_FILES += [
+ 'content/lightning.js',
+]
+
+FINAL_TARGET_FILES.timezones += [
+ '../timezones/zones.json',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Calendar', 'Lightning Only')
+
+with Files('content/suite-*'):
+ BUG_COMPONENT = ('Calendar', 'Lightning: SeaMonkey Integration')
+
+with Files('build/**'):
+ BUG_COMPONENT = ('Calendar', 'Build Config')
+
+with Files('app.ini'):
+ BUG_COMPONENT = ('Calendar', 'Build Config')
diff --git a/calendar/lightning/themes/common/html-item-editing.css b/calendar/lightning/themes/common/html-item-editing.css
new file mode 100644
index 000000000..56a27cc45
--- /dev/null
+++ b/calendar/lightning/themes/common/html-item-editing.css
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* html and body styles are copied from chrome://global/skin/global.css
+ * as a temporary measure to prevent a transparent background on windows.
+ * Using the global.css file does not work because of a namespace issue.
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=1297392#c4
+ *
+ * #container is the top-level react div
+ */
+html,
+body,
+#container {
+ -moz-appearance: window;
+ background-color: -moz-Dialog;
+ color: -moz-DialogText;
+ font: message-box;
+}
+a {
+ color: -moz-NativehyperlinkText;
+ text-decoration: underline;
+ cursor: pointer;
+}
+#topwrapper {
+ max-width: 1200px;
+}
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ padding: 10px;
+ margin-bottom: 10px;
+}
+#wrapper2 {
+ flex-direction: row;
+ flex-wrap: nowrap;
+}
+.box {
+ flex: 1 1 auto;
+ padding: 10px;
+ border-bottom: 1px solid ButtonShadow;
+ margin-bottom: 10px;
+ margin-right: 10px;
+}
+#box5 {
+ flex-basis: 80%;
+}
+#attendeesDiv {
+ page-break-after: always;
+}
+#description {
+ margin: 10px;
+}
+#descriptionTextArea {
+ width: 100%;
+ box-sizing: border-box;
+}
+#tabstrip {
+ display: flex;
+ padding: 0;
+}
+.tab {
+ list-style: none;
+ border: 1px solid ButtonShadow;
+ padding: 5px 10px;
+ cursor: pointer;
+}
+.activeTab {
+ border-bottom: 0px;
+}
+.hidden {
+ display:none;
+}
+.tabpanel {
+ min-height: 250px;
+}
+.capsule {
+ margin-right: 5px;
+ padding: 3px;
+ border-radius: 3px;
+ color: ButtonText;
+}
+.deleteCapsule {
+ cursor: pointer;
+ border-left: 1px solid ButtonText;
+ padding-left: 4px;
+ padding-right: 4px;
+ margin-left: 6px;
+ font-family: sans;
+}
+
+/* Small screens */
+@media all and (max-width: 750px) {
+ #attendeesDiv {
+ page-break-after: auto;
+ }
+}
diff --git a/calendar/lightning/themes/common/images/mode-switch-icons.png b/calendar/lightning/themes/common/images/mode-switch-icons.png
new file mode 100644
index 000000000..ee311b74b
--- /dev/null
+++ b/calendar/lightning/themes/common/images/mode-switch-icons.png
Binary files differ
diff --git a/calendar/lightning/themes/common/imip.css b/calendar/lightning/themes/common/imip.css
new file mode 100644
index 000000000..0994a86c1
--- /dev/null
+++ b/calendar/lightning/themes/common/imip.css
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.itip-icon {
+ --itip-icon-partstat: -16px -16px; /* default: NEEDS-ACTION */
+ --itip-icon-role: 0px; /* default: REQ-PARTICIPANT */
+ --itip-icon-usertype: -32px; /* default: INDIVIDUAL */
+ width: 16px;
+ height: 16px;
+ background-image: url(chrome://calendar-common/skin/calendar-itip-icons.svg),
+ url(chrome://calendar-common/skin/calendar-itip-icons.svg);
+ background-position: var(--itip-icon-partstat), var(--itip-icon-usertype) var(--itip-icon-role);
+}
+.itip-icon[partstat="ACCEPTED"] {
+ --itip-icon-partstat: 0px 0px;
+}
+.itip-icon[partstat="DECLINED"] {
+ --itip-icon-partstat: 0px -16px;
+}
+.itip-icon[partstat="DELEGATED"] {
+ --itip-icon-partstat: 0px -32px;
+}
+.itip-icon[partstat="TENTATIVE"] {
+ --itip-icon-partstat: -16px 0px;
+}
+.itip-icon[usertype="INDIVIDUAL"] {
+ --itip-icon-usertype: -32px;
+}
+.itip-icon[usertype="GROUP"] {
+ --itip-icon-usertype: -48px;
+}
+.itip-icon[usertype="RESOURCE"] {
+ --itip-icon-usertype: -64px;
+}
+.itip-icon[usertype="ROOM"] {
+ --itip-icon-usertype: -80px;
+}
+.itip-icon[usertype="UNKNOWN"] {
+ --itip-icon-usertype: -96px;
+}
+.itip-icon[role="REQ-PARTICIPANT"] {
+ --itip-icon-role: 0px;
+}
+.itip-icon[role="OPT-PARTICIPANT"] {
+ --itip-icon-role: -16px;
+}
+.itip-icon[role="NON-PARTICIPANT"] {
+ --itip-icon-role: -32px;
+}
+.itip-icon[role="CHAIR"] {
+ --itip-icon-role: -32px;
+ --itip-icon-usertype: -16px;
+}
+
+#imipHtml-attendees-row > .content,
+#imipHtml-organizer-row > .content,
+#attendee-table > tbody > tr > td,
+#organizer-table > tbody > tr > td {
+ padding: 0
+}
+
+#invitation-table {
+ border: 3px solid -moz-default-color;
+ border-collapse: collapse;
+ width: 40em;
+ margin-inline-start: auto;
+ margin-inline-end: auto;
+}
+#invitation-table > tbody > tr > td {
+ padding: 3px;
+ vertical-align: top;
+ width: 2em;
+ text-align: left;
+}
+
+.header {
+ color: HighlightText;
+ font-size: 1em;
+ font-weight: bold;
+ background-color: Highlight;
+}
+.description {
+ width: 9em;
+ text-align: right;
+ border-inline-end: 1px solid rgba(255, 255, 255, 0.2);
+ background-color: rgba(0, 0, 0, 0.2);
+ vertical-align: top;
+}
+.content {
+ width: 29em;
+ background-color: -moz-default-background-color;
+}
+.content p {
+ white-space: pre-wrap;
+}
+.added {
+ color: rgb(255, 0, 0);
+}
+.added[systemcolors] {
+ color: -moz-DialogText;
+ font-weight: bold;
+}
+.modified {
+ color: rgb(255, 0, 0);
+ font-style: italic;
+}
+.modified[systemcolors] {
+ color: -moz-DialogText;
+}
+.removed {
+ color: rgb(125, 125, 125);
+ text-decoration: line-through;
+}
+.removed[systemcolors] {
+ color: -moz-DialogText;
+}
diff --git a/calendar/lightning/themes/common/lightning.css b/calendar/lightning/themes/common/lightning.css
new file mode 100644
index 000000000..b1b0d05ae
--- /dev/null
+++ b/calendar/lightning/themes/common/lightning.css
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* avoids contributing to the min width when Lightning is not selected */
+#calendarTabPanel:not([selected]) {
+ visibility: collapse;
+}
+
+#calendar-status-todaypane-button > stack > .toolbarbutton-day-text {
+ text-align: center;
+ margin-inline-start: 0;
+ margin-bottom: -4px;
+ font-size: 7pt;
+ font-family: Arial, Helvetica, sans-serif;
+ font-weight: bold;
+ text-shadow: none;
+ background-color: transparent;
+}
+
+#calendar-status-todaypane-button[hideLabel] > .toolbarbutton-text,
+#calendar-status-todaypane-button[hideLabel] > .toolbarbutton-icon-end {
+ display: none;
+}
+
+.imipMoreButton > .toolbarbutton-icon {
+ display: none;
+}
+
+@media not all and (-moz-os-version: windows-xp) {
+ #task-tab-button .toolbarbutton-icon,
+ #calendar-tab-button .toolbarbutton-icon,
+ .calbar-toolbarbutton-1 .toolbarbutton-icon,
+ toolbarpaletteitem > .msgHeaderView-button .toolbarbutton-icon,
+ #task-actions-toolbar > .msgHeaderView-button .toolbarbutton-icon {
+ width: 18px;
+ height: 18px;
+ }
+}
+
+#calendar-toolbox,
+#task-toolbox {
+ -moz-appearance: none;
+ background-color: rgb(248, 248, 248);
+ border-bottom: 1px solid threedshadow;
+ border-top: 0px;
+} \ No newline at end of file
diff --git a/calendar/lightning/themes/common/suite-accountCentral.css b/calendar/lightning/themes/common/suite-accountCentral.css
new file mode 100644
index 000000000..2da68effd
--- /dev/null
+++ b/calendar/lightning/themes/common/suite-accountCentral.css
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+#lightning-newCalendar-row {
+ list-style-image: url("chrome://calendar/skin/cal-icon24.png");
+}
diff --git a/calendar/lightning/themes/linux/accountCentral.css b/calendar/lightning/themes/linux/accountCentral.css
new file mode 100644
index 000000000..6a628dd0b
--- /dev/null
+++ b/calendar/lightning/themes/linux/accountCentral.css
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#lightning-newCalendar-row > hbox > label:-moz-locale-dir(rtl) {
+ background-position: right !important;
+}
+
+#lightning-newCalendar-row > hbox > label {
+ background: url(chrome://calendar/skin/cal-icon24.png) no-repeat !important;
+}
diff --git a/calendar/lightning/themes/linux/imip.css b/calendar/lightning/themes/linux/imip.css
new file mode 100644
index 000000000..2d96dd4fb
--- /dev/null
+++ b/calendar/lightning/themes/linux/imip.css
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://lightning-common/skin/imip.css);
diff --git a/calendar/lightning/themes/linux/lightning-toolbar.css b/calendar/lightning/themes/linux/lightning-toolbar.css
new file mode 100644
index 000000000..a3c39a544
--- /dev/null
+++ b/calendar/lightning/themes/linux/lightning-toolbar.css
@@ -0,0 +1,130 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Lightning "Calendar" Toolbarbutton */
+
+#lightning-button-calendar {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab);
+}
+
+#lightning-button-tasks {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab);
+}
+
+#calendar-synchronize-button,
+#task-synchronize-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize);
+}
+
+#extractEventButton,
+#task-newevent-button,
+#hdrExtractEventButton,
+#calendar-newevent-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent);
+}
+
+#extractTaskButton,
+#task-newtask-button,
+#hdrExtractTaskButton,
+#calendar-newtask-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask);
+}
+
+#calendar-edit-button,
+#task-edit-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#edit);
+}
+
+#calendar-delete-button,
+#task-delete-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete);
+}
+
+#calendar-goto-today-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#today);
+}
+
+#calendar-print-button,
+#task-print-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#print);
+}
+
+#calendar-unifinder-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find);
+}
+
+toolbar[brighttext] #lightning-button-calendar {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-inverted);
+}
+
+toolbar[brighttext] #lightning-button-tasks {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-inverted);
+}
+
+toolbar[brighttext] #calendar-synchronize-button,
+toolbar[brighttext] #task-synchronize-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize-inverted);
+}
+
+toolbar[brighttext] #extractEventButton,
+toolbar[brighttext] #task-newevent-button,
+toolbar[brighttext] #hdrExtractEventButton,
+toolbar[brighttext] #calendar-newevent-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent-inverted);
+}
+
+toolbar[brighttext] #extractTaskButton,
+toolbar[brighttext] #task-newtask-button,
+toolbar[brighttext] #hdrExtractTaskButton,
+toolbar[brighttext] #calendar-newtask-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask-inverted);
+}
+
+toolbar[brighttext] #calendar-edit-button,
+toolbar[brighttext] #task-edit-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#edit-inverted);
+}
+
+toolbar[brighttext] #calendar-delete-button,
+toolbar[brighttext] #task-delete-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete-inverted);
+}
+
+toolbar[brighttext] #calendar-goto-today-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#today-inverted);
+}
+
+toolbar[brighttext] #calendar-print-button,
+toolbar[brighttext] #task-print-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#print-inverted);
+}
+
+toolbar[brighttext] #calendar-unifinder-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find-inverted);
+}
+
+#lightning-button-calendar > .toolbarbutton-icon,
+#lightning-button-tasks > .toolbarbutton-icon,
+#calendar-synchronize-button > .toolbarbutton-icon,
+#task-synchronize-button > .toolbarbutton-icon,
+#extractEventButton > .toolbarbutton-icon,
+#task-newevent-button > .toolbarbutton-icon,
+#hdrExtractEventButton > .toolbarbutton-icon,
+#calendar-newevent-button > .toolbarbutton-icon,
+#extractTaskButton > .toolbarbutton-icon,
+#task-newtask-button > .toolbarbutton-icon,
+#hdrExtractTaskButton > .toolbarbutton-icon,
+#calendar-newtask-button > .toolbarbutton-icon,
+#calendar-edit-button > .toolbarbutton-icon,
+#task-edit-button > .toolbarbutton-icon,
+#calendar-delete-button > .toolbarbutton-icon,
+#task-delete-button > .toolbarbutton-icon,
+#calendar-goto-today-button > .toolbarbutton-icon,
+#calendar-print-button > .toolbarbutton-icon,
+#task-print-button > .toolbarbutton-icon,
+#calendar-unifinder-button > .toolbarbutton-icon {
+ width: 18px;
+ height: 18px;
+ padding: 0;
+}
diff --git a/calendar/lightning/themes/linux/lightning-widgets.css b/calendar/lightning/themes/linux/lightning-widgets.css
new file mode 100644
index 000000000..0c3e1aa5e
--- /dev/null
+++ b/calendar/lightning/themes/linux/lightning-widgets.css
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+lightning-notification-bar {
+ background-color: #baeeff;
+ color: -moz-dialogtext;
+ border-bottom: 1px solid ThreeDDarkShadow;
+ padding: 3px;
+}
diff --git a/calendar/lightning/themes/linux/lightning.css b/calendar/lightning/themes/linux/lightning.css
new file mode 100644
index 000000000..c5b02f467
--- /dev/null
+++ b/calendar/lightning/themes/linux/lightning.css
@@ -0,0 +1,248 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://lightning-common/skin/lightning.css);
+
+#calendarContent {
+ color: -moz-DialogText;
+ background-color: -moz-Dialog;
+}
+
+#calsidebar_splitter,
+#today-splitter {
+ -moz-appearance: none;
+ /* splitter grip area */
+ width: 5px;
+ margin-top: 0;
+ /* because of the negative margin needed to make the splitter visible */
+ position: relative;
+ z-index: 10;
+ transition: border-width .3s ease-in;
+}
+
+#calsidebar_splitter {
+ border-inline-start: 1px solid ThreeDShadow;
+ /* make only the splitter border visible */
+ margin-inline-end: -5px;
+}
+
+#today-splitter {
+ border-inline-start: 1px solid ThreeDShadow;
+ /* make only the splitter border visible */
+ margin-inline-end: -5px;
+}
+
+#calsidebar_splitter[state="collapsed"] {
+ border-inline-start: 1px solid transparent;
+}
+
+#calsidebar_splitter[state="collapsed"]:hover {
+ border-inline-start: 4px solid highlight;
+}
+
+#today-splitter > grippy {
+ display: none;
+}
+
+/* Calendar list rules */
+#calendar-panel {
+ padding-bottom: 5px;
+}
+
+/* Lightning preferences icon */
+radio[pane=paneLightning] {
+ list-style-image: url("chrome://calendar/skin/cal-icon32.png");
+}
+
+/* iMIP notification bar */
+#imip-bar > image {
+ list-style-image: url("chrome://calendar/skin/cal-icon32.png");
+ -moz-image-region: rect(0px, 32px, 32px, 0px);
+}
+
+/* ::::: tabs ::::: */
+
+/* ::: new tab buttons ::: */
+#calendar-tab-button,
+#newMsgButton-calendar-menuitem {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab);
+}
+
+#task-tab-button,
+#newMsgButton-task-menuitem {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab);
+}
+
+#tabs-toolbar[brighttext] #calendar-tab-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-inverted);
+}
+
+#tabs-toolbar[brighttext] #task-tab-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-inverted);
+}
+
+/* ::: tab icons ::: */
+.icon-holder[type="calendar"],
+.tabmail-tab[type="calendar"] {
+ list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png);
+ -moz-image-region: rect(0px 64px 16px 48px);
+}
+
+.icon-holder[type="calendar"][selected="true"],
+.tabmail-tab[type="calendar"][selected="true"] {
+ -moz-image-region: rect(16px 64px 32px 48px);
+}
+
+.icon-holder[type="tasks"],
+.tabmail-tab[type="tasks"] {
+ list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png);
+ -moz-image-region: rect(0px 80px 16px 64px);
+}
+
+.icon-holder[type="tasks"][selected="true"],
+.tabmail-tab[type="tasks"][selected="true"] {
+ -moz-image-region: rect(16px 80px 32px 64px);
+}
+
+/* Lightning sidebar in calendar and task mode */
+#ltnSidebar {
+ background-color: -moz-field;
+ border-bottom: 1px solid ThreeDShadow;
+}
+
+/* Write button */
+#newMsgButton-mail-menuitem {
+ list-style-image: url(chrome://messenger/skin/icons/mail-toolbar.svg#newmsg);
+}
+
+#newMsgButton-mail-menuitem > .menu-iconic-left > .menu-iconic-icon,
+#newMsgButton-calendar-menuitem > .menu-iconic-left > .menu-iconic-icon,
+#newMsgButton-task-menuitem > .menu-iconic-left > .menu-iconic-icon {
+ width: 18px;
+ height: 18px;
+ margin: -1px;
+}
+
+/* Today pane button in status bar */
+#calendar-status-todaypane-button,
+#calendar-status-todaypane-button[checked="true"] {
+ min-width: 0;
+ min-height: 0;
+ margin: 1px 0 0;
+ -moz-appearance: none;
+ border-radius: 3px;
+ padding: 1px 2px 0 !important;
+ border: 1px solid transparent;
+}
+
+#calendar-status-todaypane-button:hover {
+ border: 1px solid ThreeDShadow;
+ background-color: transparent !important;
+ background-image: none;
+ -moz-appearance: none;
+}
+
+#calendar-status-todaypane-button[hideLabel] > stack {
+ margin-inline-start: 5px;
+}
+
+#calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#pane);
+ -moz-image-region: rect(0 18px 18px 0);
+}
+
+#calendar-status-todaypane-button:-moz-lwtheme-brighttext > stack >
+ .toolbarbutton-icon-begin {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#pane-inverted);
+}
+
+/* compensate the 18px icon height */
+#calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin {
+ margin-top: -1px;
+ margin-bottom: -1px;
+}
+
+#calendar-status-todaypane-button > stack > .toolbarbutton-day-text {
+ margin-top: 4px;
+}
+
+/* shift the today pane button label up by one pixel to center it */
+#calendar-status-todaypane-button > .toolbarbutton-text {
+ margin: 0 0 1px !important;
+}
+
+#calendar-status-todaypane-button > .toolbarbutton-icon-end {
+ list-style-image: url(chrome://global/skin/icons/collapse.png);
+}
+
+#calendar-status-todaypane-button[checked="true"] > .toolbarbutton-icon-end {
+ list-style-image: url(chrome://global/skin/icons/expand.png);
+}
+
+#calMinimonthBox {
+ margin-top: 3px;
+}
+
+/* ::: imip button icons ::: */
+#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button {
+ background-color: -moz-dialog;
+}
+
+#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button > .toolbarbutton-menubutton-button,
+#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button > .toolbarbutton-menubutton-dropmarker {
+ background-color: transparent;
+}
+
+#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:not(:active):hover,
+#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:-moz-any(:hover,[open="true"]) >
+ .toolbarbutton-menubutton-button,
+#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:hover >
+ .toolbarbutton-menubutton-dropmarker {
+ background: -moz-dialog linear-gradient(rgba(255, 255, 255, .5), transparent);
+}
+
+#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:not([type="menu-button"]):hover:active,
+#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button[type="menu-button"] >
+ .toolbarbutton-menubutton-button:hover:active,
+#imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button[open="true"] >
+ .toolbarbutton-menubutton-dropmarker {
+ background: rgb(154, 154, 154) linear-gradient(rgba(255, 255, 255, .7), rgba(255, 255, 255, .4));
+}
+
+.imipAcceptRecurrencesButton,
+.imipAcceptButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#complete);
+}
+
+.imipDeclineCounterButton,
+.imipDeclineRecurrencesButton,
+.imipDeclineButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#decline);
+}
+
+.imipTentativeRecurrencesButton,
+.imipTentativeButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#tentative);
+}
+
+.imipDetailsButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find);
+}
+
+.imipAddButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent);
+}
+
+.imipRescheduleButton,
+.imipUpdateButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize);
+}
+
+.imipDeleteButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete);
+}
+
+.imipReconfirmButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority);
+}
diff --git a/calendar/lightning/themes/windows/accountCentral.css b/calendar/lightning/themes/windows/accountCentral.css
new file mode 100644
index 000000000..6a628dd0b
--- /dev/null
+++ b/calendar/lightning/themes/windows/accountCentral.css
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#lightning-newCalendar-row > hbox > label:-moz-locale-dir(rtl) {
+ background-position: right !important;
+}
+
+#lightning-newCalendar-row > hbox > label {
+ background: url(chrome://calendar/skin/cal-icon24.png) no-repeat !important;
+}
diff --git a/calendar/lightning/themes/windows/images/imip-aero.png b/calendar/lightning/themes/windows/images/imip-aero.png
new file mode 100644
index 000000000..f5c303aa1
--- /dev/null
+++ b/calendar/lightning/themes/windows/images/imip-aero.png
Binary files differ
diff --git a/calendar/lightning/themes/windows/images/mode-switch-icons-aero.png b/calendar/lightning/themes/windows/images/mode-switch-icons-aero.png
new file mode 100644
index 000000000..e1a7f1b52
--- /dev/null
+++ b/calendar/lightning/themes/windows/images/mode-switch-icons-aero.png
Binary files differ
diff --git a/calendar/lightning/themes/windows/images/mode-switch-icons-inverted.png b/calendar/lightning/themes/windows/images/mode-switch-icons-inverted.png
new file mode 100644
index 000000000..c61b5a03b
--- /dev/null
+++ b/calendar/lightning/themes/windows/images/mode-switch-icons-inverted.png
Binary files differ
diff --git a/calendar/lightning/themes/windows/imip.css b/calendar/lightning/themes/windows/imip.css
new file mode 100644
index 000000000..2d96dd4fb
--- /dev/null
+++ b/calendar/lightning/themes/windows/imip.css
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://lightning-common/skin/imip.css);
diff --git a/calendar/lightning/themes/windows/imip.png b/calendar/lightning/themes/windows/imip.png
new file mode 100644
index 000000000..0690f8737
--- /dev/null
+++ b/calendar/lightning/themes/windows/imip.png
Binary files differ
diff --git a/calendar/lightning/themes/windows/lightning-toolbar.css b/calendar/lightning/themes/windows/lightning-toolbar.css
new file mode 100644
index 000000000..fcc74f1b3
--- /dev/null
+++ b/calendar/lightning/themes/windows/lightning-toolbar.css
@@ -0,0 +1,432 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#calendar-toolbar2,
+#task-toolbar2 {
+ border-top-width: 0;
+}
+
+@media (-moz-windows-glass) {
+ #calendar-toolbox:not(:-moz-lwtheme) {
+ border-bottom-color: #AABCCF;
+ }
+}
+
+@media (-moz-os-version: windows-xp) {
+ #lightning-button-calendar {
+ list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png);
+ -moz-image-region: rect(0px 24px 24px 0px);
+ }
+
+ #lightning-button-calendar[disabled] {
+ -moz-image-region: rect(48px 24px 72px 0px);
+ }
+ toolbar[iconsize="small"] #lightning-button-calendar {
+ -moz-image-region: rect(0px 64px 16px 48px);
+ }
+
+ toolbar[iconsize="small"] #lightning-button-calendar[disabled] {
+ -moz-image-region: rect(32px 64px 48px 48px);
+ }
+
+ /* Lightning "Tasks" Toolbarbutton */
+ #lightning-button-tasks {
+ list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png);
+ -moz-image-region: rect(0px 48px 24px 24px);
+ }
+
+ #lightning-button-tasks[disabled] {
+ -moz-image-region: rect(48px 48px 72px 24px);
+ }
+
+ toolbar[iconsize="small"] #lightning-button-tasks {
+ -moz-image-region: rect(0px 80px 16px 64px);
+ }
+ toolbar[iconsize="small"] #lightning-button-tasks[disabled] {
+ -moz-image-region: rect(32px 80px 48px 64px);
+ }
+
+ /* Toolbar buttons */
+
+ .calbar-toolbarbutton-1 {
+ list-style-image: url(chrome://calendar/skin/toolbar-large.png);
+ }
+
+ toolbar[iconsize="small"] .calbar-toolbarbutton-1 {
+ list-style-image: url(chrome://calendar/skin/toolbar-small.png);
+ }
+
+ #calendar-synchronize-button,
+ #task-synchronize-button {
+ -moz-image-region: rect(0px 648px 24px 624px);
+ }
+
+ #calendar-synchronize-button[disabled],
+ #task-synchronize-button[disabled] {
+ -moz-image-region: rect(48px 648px 72px 624px);
+ }
+
+ toolbar[iconsize="small"] #calendar-synchronize-button,
+ toolbar[iconsize="small"] #task-synchronize-button {
+ -moz-image-region: rect(0px 432px 16px 416px);
+ }
+
+ toolbar[iconsize="small"] #calendar-synchronize-button[disabled],
+ toolbar[iconsize="small"] #task-synchronize-button[disabled] {
+ -moz-image-region: rect(32px 432px 48px 416px);
+ }
+
+ #calendar-newevent-button,
+ #task-newevent-button {
+ -moz-image-region: rect(0px 24px 24px 0px);
+ }
+
+ #calendar-newevent-button[disabled],
+ #task-newevent-button[disabled] {
+ -moz-image-region: rect(48px 24px 72px 0px);
+ }
+
+ toolbar[iconsize="small"] #calendar-newevent-button,
+ toolbar[iconsize="small"] #task-newevent-button {
+ -moz-image-region: rect(0px 16px 16px 0px);
+ }
+
+ toolbar[iconsize="small"] #calendar-newevent-button[disabled],
+ toolbar[iconsize="small"] #task-newevent-button[disabled] {
+ -moz-image-region: rect(32px 16px 48px 0px);
+ }
+
+ #calendar-newtask-button,
+ #task-newtask-button {
+ -moz-image-region: rect(0px 384px 24px 360px);
+ }
+
+ #calendar-newtask-button[disabled],
+ #task-newtask-button[disabled] {
+ -moz-image-region: rect(48px 384px 72px 360px);
+ }
+
+ toolbar[iconsize="small"] #calendar-newtask-button,
+ toolbar[iconsize="small"] #task-newtask-button {
+ -moz-image-region: rect(0px 256px 16px 240px);
+ }
+
+ toolbar[iconsize="small"] #calendar-newtask-button[disabled],
+ toolbar[iconsize="small"] #task-newtask-button[disabled] {
+ -moz-image-region: rect(32px 256px 48px 240px);
+ }
+
+ #calendar-edit-button,
+ #task-edit-button {
+ -moz-image-region: rect(0px 48px 24px 24px);
+ }
+
+ #calendar-edit-button[disabled],
+ #task-edit-button[disabled] {
+ -moz-image-region: rect(48px 48px 72px 24px);
+ }
+
+ toolbar[iconsize="small"] #calendar-edit-button,
+ toolbar[iconsize="small"] #task-edit-button {
+ -moz-image-region: rect(0px 32px 16px 16px);
+ }
+
+ toolbar[iconsize="small"] #calendar-edit-button[disabled],
+ toolbar[iconsize="small"] #task-edit-button[disabled] {
+ -moz-image-region: rect(32px 32px 48px 16px);
+ }
+
+ #calendar-delete-button,
+ #task-delete-button {
+ -moz-image-region: rect(0px 72px 24px 48px);
+ }
+
+ #calendar-delete-button[disabled],
+ #task-delete-button[disabled] {
+ -moz-image-region: rect(48px 72px 72px 48px);
+ }
+
+ toolbar[iconsize="small"] #calendar-delete-button,
+ toolbar[iconsize="small"] #task-delete-button {
+ -moz-image-region: rect(0px 48px 16px 32px);
+ }
+
+ toolbar[iconsize="small"] #calendar-delete-button[disabled],
+ toolbar[iconsize="small"] #task-delete-button[disabled] {
+ -moz-image-region: rect(32px 48px 48px 32px);
+ }
+
+ #calendar-goto-today-button {
+ -moz-image-region: rect(0px 408px 24px 384px);
+ }
+
+ #calendar-goto-today-button[disabled] {
+ -moz-image-region: rect(48px 408px 72px 384px);
+ }
+
+ toolbar[iconsize="small"] #calendar-goto-today-button {
+ -moz-image-region: rect(0px 272px 16px 256px);
+ }
+
+ toolbar[iconsize="small"] #calendar-goto-today-button[disabled] {
+ -moz-image-region: rect(32px 272px 48px 256px);
+ }
+
+ #calendar-print-button,
+ #task-print-button {
+ -moz-image-region: rect(0px 360px 24px 336px);
+ }
+
+ #calendar-print-button[disabled],
+ #task-print-button[disabled] {
+ -moz-image-region: rect(48px 360px 72px 336px);
+ }
+
+ toolbar[iconsize="small"] #calendar-print-button,
+ toolbar[iconsize="small"] #task-print-button {
+ -moz-image-region: rect(0px 240px 16px 224px);
+ }
+
+ toolbar[iconsize="small"] #calendar-print-button[disabled],
+ toolbar[iconsize="small"] #task-print-button[disabled] {
+ -moz-image-region: rect(32px 240px 48px 224px);
+ }
+
+ #calendar-unifinder-button {
+ -moz-image-region: rect(0px 528px 24px 504px);
+ }
+
+ #calendar-unifinder-button[disabled] {
+ -moz-image-region: rect(48px 528px 72px 504px);
+ }
+
+ toolbar[iconsize="small"] #calendar-unifinder-button {
+ -moz-image-region: rect(0px 352px 16px 336px);
+ }
+
+ toolbar[iconsize="small"] #calendar-unifinder-button[disabled] {
+ -moz-image-region: rect(32px 352px 48px 336px);
+ }
+
+ #hdrExtractEventButton {
+ list-style-image: url("chrome://calendar/skin/toolbar-small.png");
+ -moz-image-region: rect(0px, 16px, 16px, 0px);
+ }
+
+ #hdrExtractTaskButton {
+ list-style-image: url("chrome://calendar/skin/toolbar-small.png");
+ -moz-image-region: rect(0px, 256px, 16px, 240px);
+ }
+
+ #extractEventButton {
+ list-style-image: url("chrome://calendar/skin/toolbar-large.png");
+ -moz-image-region: rect(0px, 24px, 24px, 0px);
+ }
+
+ #extractTaskButton {
+ list-style-image: url("chrome://calendar/skin/toolbar-large.png");
+ -moz-image-region: rect(0px, 384px, 24px, 360px);
+ }
+
+ #extractEventButton[disabled] {
+ -moz-image-region: rect(48px, 24px, 72px, 0px);
+ }
+
+ #extractTaskButton[disabled] {
+ -moz-image-region: rect(48px, 384px, 72px, 360px);
+ }
+
+ toolbar[iconsize="small"] #extractEventButton {
+ list-style-image: url("chrome://calendar/skin/toolbar-small.png");
+ -moz-image-region: rect(0px, 16px, 16px, 0px);
+ }
+
+ toolbar[iconsize="small"] #extractTaskButton {
+ list-style-image: url("chrome://calendar/skin/toolbar-small.png");
+ -moz-image-region: rect(0px, 256px, 16px, 240px);
+ }
+
+ toolbar[iconsize="small"] #extractEventButton[disabled] {
+ -moz-image-region: rect(32px, 16px, 48px, 0px);
+ }
+
+ toolbar[iconsize="small"] #extractTaskButton[disabled] {
+ -moz-image-region: rect(32px, 256px, 48px, 240px);
+ }
+}
+
+@media not all and (-moz-os-version: windows-xp) {
+ #lightning-button-calendar {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab);
+ }
+
+ #lightning-button-tasks {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab);
+ }
+
+ #calendar-synchronize-button,
+ #task-synchronize-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize);
+ }
+
+ #extractEventButton,
+ #task-newevent-button,
+ #hdrExtractEventButton,
+ #calendar-newevent-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent);
+ }
+
+ #extractTaskButton,
+ #task-newtask-button,
+ #hdrExtractTaskButton,
+ #calendar-newtask-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask);
+ }
+
+ #calendar-edit-button,
+ #task-edit-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#edit);
+ }
+
+ #calendar-delete-button,
+ #task-delete-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete);
+ }
+
+ #calendar-goto-today-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#today);
+ }
+
+ #calendar-print-button,
+ #task-print-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#print);
+ }
+
+ #calendar-unifinder-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find);
+ }
+
+ toolbar[brighttext] #lightning-button-calendar {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-inverted);
+ }
+
+ toolbar[brighttext] #lightning-button-tasks {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-inverted);
+ }
+
+ toolbar[brighttext] #calendar-synchronize-button,
+ toolbar[brighttext] #task-synchronize-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize-inverted);
+ }
+
+ toolbar[brighttext] #extractEventButton,
+ toolbar[brighttext] #task-newevent-button,
+ toolbar[brighttext] #hdrExtractEventButton,
+ toolbar[brighttext] #calendar-newevent-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent-inverted);
+ }
+
+ toolbar[brighttext] #extractTaskButton,
+ toolbar[brighttext] #task-newtask-button,
+ toolbar[brighttext] #hdrExtractTaskButton,
+ toolbar[brighttext] #calendar-newtask-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask-inverted);
+ }
+
+ toolbar[brighttext] #calendar-edit-button,
+ toolbar[brighttext] #task-edit-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#edit-inverted);
+ }
+
+ toolbar[brighttext] #calendar-delete-button,
+ toolbar[brighttext] #task-delete-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete-inverted);
+ }
+
+ toolbar[brighttext] #calendar-goto-today-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#today-inverted);
+ }
+
+ toolbar[brighttext] #calendar-print-button,
+ toolbar[brighttext] #task-print-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#print-inverted);
+ }
+
+ toolbar[brighttext] #calendar-unifinder-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find-inverted);
+ }
+}
+
+@media (-moz-windows-default-theme) and (-moz-os-version: windows-win8),
+ (-moz-windows-default-theme) and (-moz-os-version: windows-win10) {
+ #lightning-button-calendar {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-flat);
+ }
+
+ #lightning-button-tasks {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-flat);
+ }
+
+ #calendar-synchronize-button,
+ #task-synchronize-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize-flat);
+ }
+
+ #extractEventButton,
+ #task-newevent-button,
+ #hdrExtractEventButton,
+ #calendar-newevent-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent-flat);
+ }
+
+ #extractTaskButton,
+ #task-newtask-button,
+ #hdrExtractTaskButton,
+ #calendar-newtask-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newtask-flat);
+ }
+
+ #calendar-edit-button,
+ #task-edit-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#edit-flat);
+ }
+
+ #calendar-goto-today-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#today-flat);
+ }
+
+ #calendar-print-button,
+ #task-print-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#print-flat);
+ }
+
+ #calendar-unifinder-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find-flat);
+ }
+}
+
+#lightning-button-calendar > .toolbarbutton-icon,
+#lightning-button-tasks > .toolbarbutton-icon,
+#calendar-synchronize-button > .toolbarbutton-icon,
+#task-synchronize-button > .toolbarbutton-icon,
+#extractEventButton > .toolbarbutton-icon,
+#task-newevent-button > .toolbarbutton-icon,
+#hdrExtractEventButton > .toolbarbutton-icon,
+#calendar-newevent-button > .toolbarbutton-icon,
+#extractTaskButton > .toolbarbutton-icon,
+#task-newtask-button > .toolbarbutton-icon,
+#hdrExtractTaskButton > .toolbarbutton-icon,
+#calendar-newtask-button > .toolbarbutton-icon,
+#calendar-edit-button > .toolbarbutton-icon,
+#task-edit-button > .toolbarbutton-icon,
+#calendar-delete-button > .toolbarbutton-icon,
+#task-delete-button > .toolbarbutton-icon,
+#calendar-goto-today-button > .toolbarbutton-icon,
+#calendar-print-button > .toolbarbutton-icon,
+#task-print-button > .toolbarbutton-icon,
+#calendar-unifinder-button > .toolbarbutton-icon {
+ width: 18px;
+ height: 18px;
+ padding: 0;
+}
diff --git a/calendar/lightning/themes/windows/lightning-widgets.css b/calendar/lightning/themes/windows/lightning-widgets.css
new file mode 100644
index 000000000..0c3e1aa5e
--- /dev/null
+++ b/calendar/lightning/themes/windows/lightning-widgets.css
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+lightning-notification-bar {
+ background-color: #baeeff;
+ color: -moz-dialogtext;
+ border-bottom: 1px solid ThreeDDarkShadow;
+ padding: 3px;
+}
diff --git a/calendar/lightning/themes/windows/lightning.css b/calendar/lightning/themes/windows/lightning.css
new file mode 100644
index 000000000..f7f32dd1e
--- /dev/null
+++ b/calendar/lightning/themes/windows/lightning.css
@@ -0,0 +1,420 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url(chrome://lightning-common/skin/lightning.css);
+
+#calendarContent {
+ color: -moz-DialogText;
+ background-color: -moz-Dialog;
+}
+
+/* Calendar list rules */
+#calendar-panel {
+ padding-bottom: 5px;
+}
+
+/* Lightning preferences icon */
+radio[pane=paneLightning] {
+ list-style-image: url(chrome://calendar/skin/cal-icon32.png);
+}
+
+/* iMIP notification bar */
+#imip-bar > image {
+ list-style-image: url(chrome://calendar/skin/cal-icon32.png);
+ -moz-image-region: rect(0 32px 32px 0);
+}
+
+/* Lightning sidebar background in calendar and task mode */
+#ltnSidebar {
+ background-color: -moz-field;
+}
+
+/* Today pane button in status bar */
+#calendar-status-todaypane-button,
+#calendar-status-todaypane-button[checked="true"] {
+ min-width: 0;
+ min-height: 0;
+ margin: 1px 0 0;
+ -moz-appearance: none;
+ border-radius: 3px;
+ padding: 1px 2px 0 !important;
+ border: 1px solid transparent;
+}
+
+#calendar-status-todaypane-button:hover {
+ border: 1px solid ThreeDShadow;
+ background-color: transparent !important;
+ background-image: none;
+ -moz-appearance: none;
+}
+
+#calendar-status-todaypane-button[hideLabel] > stack {
+ margin-inline-start: 5px;
+}
+
+#calendar-status-todaypane-button > stack > .toolbarbutton-day-text {
+ margin-top: 4px;
+}
+
+#calendar-status-todaypane-button > .toolbarbutton-icon-end {
+ list-style-image: url(chrome://global/skin/icons/collapse.png);
+}
+
+#calendar-status-todaypane-button[checked="true"] > .toolbarbutton-icon-end {
+ list-style-image: url(chrome://global/skin/icons/expand.png);
+}
+
+/* shift the today pane button label up by one pixel to center it */
+#calendar-status-todaypane-button > .toolbarbutton-text {
+ margin: 0 0 1px !important;
+}
+
+#calMinimonthBox {
+ margin-top: 3px;
+}
+
+@media (-moz-os-version: windows-xp) {
+ #calsidebar_splitter {
+ border-right: none;
+ }
+
+ #today-splitter {
+ border-right: none;
+ }
+
+ #today-splitter:-moz-lwtheme {
+ background-image: linear-gradient(rgba(255, 255, 255, 0.5),
+ rgba(255, 255, 255, 0) 19px, rgba(255, 255, 255, 0) 25px,
+ ThreeDDarkShadow 25px, -moz-Dialog 26px);
+ }
+
+ #newMsgButton-mail-menuitem {
+ list-style-image: url(chrome://messenger/skin/icons/mail-toolbar-small.png);
+ -moz-image-region: rect(0 32px 16px 16px);
+ }
+
+ #newMsgButton-calendar-menuitem {
+ list-style-image: url(chrome://calendar/skin/toolbar-small.png);
+ -moz-image-region: rect(0 16px 16px 0);
+ }
+
+ #newMsgButton-task-menuitem {
+ list-style-image: url(chrome://calendar/skin/toolbar-small.png);
+ -moz-image-region: rect(0 256px 16px 240px);
+ }
+
+ #calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin {
+ list-style-image: url(chrome://calendar/skin/toolbar-small.png);
+ -moz-image-region: rect(0 336px 16px 320px);
+ }
+
+ /* ::: new tab buttons ::: */
+ #calendar-tab-button {
+ list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png);
+ -moz-image-region: rect(0 64px 16px 48px);
+ }
+
+ #calendar-tab-button[disabled] {
+ -moz-image-region: rect(32px 64px 48px 48px);
+ }
+
+ #task-tab-button {
+ list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png);
+ -moz-image-region: rect(0 80px 16px 64px);
+ }
+
+ #task-tab-button[disabled] {
+ -moz-image-region: rect(32px 80px 48px 64px);
+ }
+
+ /* ::: tab icons ::: */
+ .icon-holder[type="calendar"],
+ .tabmail-tab[type="calendar"] {
+ list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png);
+ -moz-image-region: rect(0 64px 16px 48px);
+ }
+
+ .icon-holder[type="tasks"],
+ .tabmail-tab[type="tasks"] {
+ list-style-image: url(chrome://lightning-common/skin/mode-switch-icons.png);
+ -moz-image-region: rect(0 80px 16px 64px);
+ }
+
+ /* ::: imip button icons ::: */
+ .imipAcceptButton,
+ .imipAcceptRecurrencesButton {
+ list-style-image: url(chrome://lightning/skin/imip.png);
+ -moz-image-region: rect(0 16px 16px 0);
+ }
+
+ .imipDeclineButton,
+ .imipDeclineRecurrencesButton {
+ list-style-image: url(chrome://lightning/skin/imip.png);
+ -moz-image-region: rect(0 32px 16px 16px);
+ }
+
+ .imipTentativeButton,
+ .imipTentativeRecurrencesButton {
+ list-style-image: url(chrome://lightning/skin/imip.png);
+ -moz-image-region: rect(0 48px 16px 32px);
+ }
+
+ .imipAddButton {
+ list-style-image: url(chrome://calendar/skin/toolbar-small.png);
+ -moz-image-region: rect(0 16px 16px 0);
+ }
+
+ .imipUpdateButton {
+ list-style-image: url(chrome://calendar/skin/toolbar-small.png);
+ -moz-image-region: rect(0 432px 16px 416px);
+ }
+
+ .imipDetailsButton {
+ list-style-image: url(chrome://calendar/skin/toolbar-small.png);
+ -moz-image-region: rect(0 352px 16px 336px);
+ }
+
+ .imipDeleteButton {
+ list-style-image: url(chrome://calendar/skin/toolbar-small.png);
+ -moz-image-region: rect(0 48px 16px 32px);
+ }
+
+ .imipReconfirmButton {
+ list-style-image: url(chrome://calendar/skin/tasks-actions.png);
+ -moz-image-region: rect(0 48px 16px 32px);
+ }
+}
+
+@media not all and (-moz-os-version: windows-xp) {
+ #calendar-tab-button,
+ #newMsgButton-calendar-menuitem {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab);
+ -moz-image-region: auto;
+ }
+
+ #task-tab-button,
+ #newMsgButton-task-menuitem {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab);
+ -moz-image-region: auto;
+ }
+
+ #newMsgButton-mail-menuitem {
+ list-style-image: url(chrome://messenger/skin/icons/mail-toolbar.svg#newmsg);
+ }
+
+ #tabs-toolbar[brighttext] #calendar-tab-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-inverted);
+ }
+
+ #tabs-toolbar[brighttext] #task-tab-button {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-inverted);
+ }
+
+ #calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#pane);
+ -moz-image-region: rect(0 18px 18px 0);
+ }
+
+#calendar-status-todaypane-button:-moz-lwtheme-brighttext > stack >
+ .toolbarbutton-icon-begin {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#pane-inverted);
+ }
+ /* compensate the 18px icon height */
+ #calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin {
+ margin-top: -1px;
+ margin-bottom: -1px;
+ }
+
+ .icon-holder[type="calendar"],
+ .tabmail-tab[type="calendar"] {
+ list-style-image: url(chrome://lightning/skin/mode-switch-icons-aero.png);
+ -moz-image-region: rect(16px 16px 32px 0);
+ }
+
+ .icon-holder[type="tasks"],
+ .tabmail-tab[type="tasks"] {
+ list-style-image: url(chrome://lightning/skin/mode-switch-icons-aero.png);
+ -moz-image-region: rect(16px 32px 32px 16px);
+ }
+
+ /* ::: imip button icons ::: */
+ .imipAcceptButton,
+ .imipAcceptRecurrencesButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#complete);
+ }
+
+ .imipDeclineCounterButton,
+ .imipDeclineButton,
+ .imipDeclineRecurrencesButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#decline);
+ }
+
+ .imipTentativeButton,
+ .imipTentativeRecurrencesButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#tentative);
+ }
+
+ .imipAddButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent);
+ }
+
+ .imipRescheduleButton,
+ .imipUpdateButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize);
+ }
+
+ .imipDetailsButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find);
+ }
+
+ .imipDeleteButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#delete);
+ }
+
+ .imipReconfirmButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority);
+ }
+
+ #newMsgButton-mail-menuitem > .menu-iconic-left > .menu-iconic-icon,
+ #newMsgButton-calendar-menuitem > .menu-iconic-left > .menu-iconic-icon,
+ #newMsgButton-task-menuitem > .menu-iconic-left > .menu-iconic-icon {
+ width: 18px;
+ height: 18px;
+ margin: -1px;
+ }
+
+ #calsidebar_splitter,
+ #today-splitter {
+ border: none;
+ min-width: 0;
+ width: 5px;
+ background-color: transparent;
+ margin-top: 0;
+ position: relative;
+ z-index: 10;
+ transition: border-width .3s ease-in;
+ }
+
+ #calsidebar_splitter {
+ border-inline-start: 1px solid #a9b7c9;
+ margin-inline-end: -5px;
+ }
+
+ #calsidebar_splitter[state="collapsed"] {
+ border-inline-start: 1px solid transparent;
+ }
+
+ #calsidebar_splitter[state="collapsed"]:hover {
+ border-inline-start: 4px solid highlight;
+ }
+
+ #today-splitter {
+ border-inline-end: 1px solid #a9b7c9;
+ margin-inline-start: -5px;
+ position: relative;
+ }
+
+ #today-splitter.calendar-sidebar-splitter:-moz-lwtheme {
+ background-image: none;
+ }
+
+ #today-splitter > grippy {
+ display: none;
+ }
+
+ #today-pane-splitter {
+ border: none;
+ border-bottom: 3px double #a9b7c9;
+ min-height: 0;
+ height: 5px;
+ background-color: transparent;
+ margin-top: -3px;
+ position: relative;
+ z-index: 10;
+ }
+}
+
+@media (-moz-windows-default-theme) and (-moz-os-version: windows-win8),
+ (-moz-windows-default-theme) and (-moz-os-version: windows-win10) {
+ #calendar-tab-button,
+ #newMsgButton-calendar-menuitem {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#calendar-tab-flat);
+ -moz-image-region: auto;
+ }
+
+ #task-tab-button,
+ #newMsgButton-task-menuitem {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#task-tab-flat);
+ -moz-image-region: auto;
+ }
+
+ #newMsgButton-mail-menuitem {
+ list-style-image: url(chrome://messenger/skin/icons/mail-toolbar.svg#newmsg-flat);
+ }
+
+ #calendar-status-todaypane-button > stack > .toolbarbutton-icon-begin {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#pane-flat);
+ }
+
+ /* ::: imip button icons ::: */
+ .imipAcceptButton,
+ .imipAcceptRecurrencesButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#complete-flat);
+ }
+
+ .imipDeclineCounterButton,
+ .imipDeclineButton,
+ .imipDeclineRecurrencesButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#decline-flat);
+ }
+
+ .imipTentativeButton,
+ .imipTentativeRecurrencesButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#tentative-flat);
+ }
+
+ .imipAddButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#newevent-flat);
+ }
+
+ .imipRescheduleButton,
+ .imipUpdateButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#synchronize-flat);
+ }
+
+ .imipDetailsButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#find-flat);
+ }
+
+ .imipReconfirmButton {
+ list-style-image: url(chrome://calendar-common/skin/calendar-toolbar.svg#priority-flat);
+ }
+}
+
+@media (-moz-windows-default-theme) {
+ #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button,
+ #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button > .toolbarbutton-menubutton-button,
+ #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button > .toolbarbutton-menubutton-dropmarker {
+ border-color: var(--toolbarbutton-active-bordercolor);
+ background-image: linear-gradient(-moz-dialog, -moz-dialog);
+ }
+
+ #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:not(:active):hover,
+ #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:-moz-any(:hover,[open="true"]) >
+ .toolbarbutton-menubutton-button,
+ #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:hover >
+ .toolbarbutton-menubutton-dropmarker {
+ background-image: linear-gradient(rgba(0, 0, 0, .1), rgba(0, 0, 0, .1)),
+ linear-gradient(-moz-dialog, -moz-dialog);
+ }
+
+ #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button:not([type="menu-button"]):hover:active,
+ #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button[type="menu-button"] >
+ .toolbarbutton-menubutton-button:hover:active,
+ #imip-view-toolbar > .toolbarbutton-1.msgHeaderView-button[open="true"] >
+ .toolbarbutton-menubutton-dropmarker {
+ background-image: linear-gradient(rgba(0, 0, 0, .15), rgba(0, 0, 0, .15)),
+ linear-gradient(-moz-dialog, -moz-dialog);
+ }
+}
diff --git a/calendar/lightning/versions.mk b/calendar/lightning/versions.mk
new file mode 100644
index 000000000..1445bdc1f
--- /dev/null
+++ b/calendar/lightning/versions.mk
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Lighting version number
+THUNDERBIRD_VERSION := $(MOZ_APP_VERSION)
+THUNDERBIRD_MAXVERSION := $(MOZ_APP_VERSION)
+SEAMONKEY_VERSION := $(MOZ_APP_MAXVERSION)
+SEAMONKEY_MAXVERSION := $(MOZ_APP_MAXVERSION)
+LIGHTNING_VERSION := $(shell $(PYTHON) $(topsrcdir)/calendar/lightning/build/makeversion.py $(word 1,$(MOZ_PKG_VERSION) $(THUNDERBIRD_VERSION)))
+GDATA_VERSION := $(shell $(PYTHON) $(topsrcdir)/calendar/providers/gdata/makeversion.py $(LIGHTNING_VERSION))