Initial AngularJS version of Webmail (desktop)

This commit is contained in:
Francis Lachapelle
2014-11-25 16:09:55 -05:00
parent 1c9da902d3
commit 2f17e94fef
14 changed files with 995 additions and 169 deletions

View File

@@ -16,6 +16,7 @@ module.exports = function(grunt) {
'css/app.css': 'scss/app.scss',
'css/SOGoRootPage.css': 'scss/SOGoRootPage.scss',
'css/ContactsUI.css': 'scss/ContactsUI.scss',
'css/MailerUI.css': 'scss/MailerUI.scss',
'css/mobile.css': 'scss/mobile.scss'
}
}

View File

@@ -117,6 +117,24 @@
return deferred.promise;
};
/**
* @function post
* @memberof Resource.prototype
* @desc Post a resource attributes on the server.
* @return a promise
*/
Resource.prototype.post = function(id, action, data) {
var deferred = this._q.defer(),
path = this._path + '/' + id + '/' + action;
this._http
.post(path, data)
.success(deferred.resolve)
.error(deferred.reject);
return deferred.promise;
};
/**
* @function save
* @memberof Resource.prototype
@@ -125,15 +143,9 @@
*/
Resource.prototype.save = function(id, newValue, options) {
var deferred = this._q.defer(),
action = (options && options.action)? options.action : 'save',
path = this._path + '/' + id + '/' + action;
action = (options && options.action)? options.action : 'save';
this._http
.post(path, newValue)
.success(deferred.resolve)
.error(deferred.reject);
return deferred.promise;
return this.post(id, action, newValue);
};
/**

View File

@@ -1,5 +1,5 @@
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* JavaScript for common UI services for mobile theme */
/* JavaScript for common UI services */
(function() {
'use strict';

View File

@@ -199,7 +199,6 @@
// link.on('dblclick', function() {
// });
scope.$on('sgSelectFolder', function(event, folderId) {
console.debug('select folder ' + folderId + ' = ' + scope.folder.id);
if (folderId == scope.folder.id) {
var list = iElement.parent();
while (list[0].tagName != 'UL') {
@@ -367,7 +366,6 @@
});
};
$scope.selectFolder = function(folder) {
console.debug("select folder " + folder.displayName);
$scope.onFolderSelect(folder);
};
}],

View File

@@ -55,9 +55,10 @@
* @memberof User.prototype
* @return the fullname along with the email address
*/
User.prototype.$shortFormat = function() {
User.prototype.$shortFormat = function(options) {
var fullname = this.cn || this.c_email;
if (this.c_email && fullname != this.c_email) {
var no_email = options && options.email === false;
if (!no_email && this.c_email && fullname != this.c_email) {
fullname += ' (' + this.c_email + ')';
}
return fullname;

View File

@@ -71,6 +71,25 @@ String.prototype.base64decode = function() {
return output;
};
String.prototype.asCSSIdentifier = function() {
var characters = [ '_' , '\\.', '#' , '@' , '\\*', ':' , ',' , ' '
, "'", '&', '\\+' ];
var escapeds = [ '_U_', '_D_', '_H_', '_A_', '_S_', '_C_', '_CO_',
'_SP_', '_SQ_', '_AM_', '_P_' ];
var newString = this;
for (var i = 0; i < characters.length; i++) {
var re = new RegExp(characters[i], 'g');
newString = newString.replace(re, escapeds[i]);
}
if (/^\d+/.test(newString)) {
newString = '_' + newString;
}
return newString;
};
function l() {
var key = arguments[0];
var value = key;

View File

@@ -0,0 +1,74 @@
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
(function() {
'use strict';
/**
* @name Account
* @constructor
* @param {object} futureAccountData
*/
function Account(futureAccountData) {
// Data is immediately available
if (typeof futureAccountData.then !== 'function') {
angular.extend(this, futureAccountData);
Account.$log.debug('Account:' + JSON.stringify(futureAccountData, undefined, 2));
}
else {
// The promise will be unwrapped first
//this.$unwrap(futureAccountData);
}
}
/**
* @memberof Account
* @desc The factory we'll use to register with Angular
* @returns the Account constructor
*/
Account.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'sgResource', 'sgMailbox', function($q, $timeout, $log, Settings, Resource, Mailbox) {
angular.extend(Account, {
$q: $q,
$timeout: $timeout,
$log: $log,
$$resource: new Resource(Settings.baseURL, Settings.activeUser),
$Mailbox: Mailbox
});
return Account; // return constructor
}];
/* Factory registration in Angular module */
angular.module('SOGo.MailerUI')
.factory('sgAccount', Account.$factory);
/**
* @memberof Account
* @desc Set the list of accounts and instanciate a new Account object for each item.
* @param {array} [data] - the metadata of the accounts
* @returns the list of accounts
*/
Account.$findAll = function(data) {
var collection = [];
if (data) {
// Each entry is spun up as an Account instance
angular.forEach(data, function(o, i) {
o.id = i;
collection[i] = new Account(o);
});
}
return collection;
};
/**
* @function $getMailboxes
* @memberof Account.prototype
* @desc Fetch the list of mailboxes for the current account.
* @returns a promise of the HTTP operation
*/
Account.prototype.$getMailboxes = function() {
var mailboxes = Account.$Mailbox.$find(this.id);
return mailboxes;
};
})();

View File

@@ -0,0 +1,300 @@
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
(function() {
'use strict';
/**
* @name Mailbox
* @constructor
* @param {object} futureMailboxData - either an object literal or a promise
*/
function Mailbox(accountId, futureMailboxData) {
this.accountId = accountId;
// Data is immediately available
if (typeof futureMailboxData.then !== 'function') {
angular.extend(this, futureMailboxData);
this.id = this.$id();
}
else {
// The promise will be unwrapped first
// NOTE: this condition never happen for the moment
this.$unwrap(futureMailboxData);
}
}
/**
* @memberof Mailbox
* @desc The factory we'll use to register with Angular
* @returns the Mailbox constructor
*/
Mailbox.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'sgResource', 'sgMessage', 'sgMailbox_PRELOAD', function($q, $timeout, $log, Settings, Resource, Message, PRELOAD) {
angular.extend(Mailbox, {
$q: $q,
$timeout: $timeout,
$log: $log,
$$resource: new Resource(Settings.baseURL, Settings.activeUser),
$Message: Message,
PRELOAD: PRELOAD
});
return Mailbox; // return constructor
}];
angular.module('SOGo.MailerUI')
/* Factory constants */
.constant('sgMailbox_PRELOAD', {
LOOKAHEAD: 50,
SIZE: 100
})
/* Factory registration in Angular module */
.factory('sgMailbox', Mailbox.$factory);
/**
* @memberof Mailbox
* @desc Fetch list of mailboxes of a specific account
* @param {string} accountId - the account ID
* @see {@link Account.$getMailboxes}
*/
Mailbox.$find = function(accountId) {
var path, futureMailboxData;
path = Mailbox.$absolutePath(accountId);
futureMailboxData = this.$$resource.post(path, 'view', {sortingAttributes: {sort: 'date', asc: false}});
return Mailbox.$unwrapCollection(accountId, futureMailboxData); // a collection of mailboxes
};
/**
* @memberof Mailbox
* @desc Unwrap to a collection of Mailbox instances.
* @param {string} accountId - the account ID
* @param {promise} futureMailboxData - a promise of the mailboxes metadata
* @returns a promise of a collection of Mailbox objects
*/
Mailbox.$unwrapCollection = function(accountId, futureMailboxData) {
var collection = [],
// Local recursive function
createMailboxes = function(mailbox) {
for (var i = 0; i < mailbox.children.length; i++) {
mailbox.children[i] = new Mailbox(accountId, mailbox.children[i]);
createMailboxes(mailbox.children[i]);
}
};
//collection.$futureMailboxData = futureMailboxData;
return futureMailboxData.then(function(data) {
return Mailbox.$timeout(function() {
// Each entry is spun up as a Mailbox instance
angular.forEach(data.mailboxes, function(data, index) {
var mailbox = new Mailbox(accountId, data);
createMailboxes(mailbox); // recursively create all sub-mailboxes
collection.push(mailbox);
});
return collection;
});
});
};
/**
* @memberof Mailbox
* @desc Build the path of the mailbox (or account only).
* @param {string} accountId - the account ID
* @param {string} [mailboxPath] - an array of the mailbox path components
* @returns a string representing the path relative to the mail module
*/
Mailbox.$absolutePath = function(accountId, mailboxPath) {
var path = [];
if (mailboxPath) {
path = _.map(mailboxPath.split('/'), function(component) {
return 'folder' + component.asCSSIdentifier();
});
}
path.splice(0, 0, accountId); // insert account ID
return path.join('/');
};
/**
* @function $id
* @memberof Mailbox.prototype
* @desc Build the unique ID to identified the mailbox.
* @returns a string representing the path relative to the mail module
*/
Mailbox.prototype.$id = function() {
return Mailbox.$absolutePath(this.accountId, this.path);
};
/**
* @function $update
* @memberof Mailbox.prototype
* @desc Fetch the messages metadata of the mailbox.
* @returns a promise of the HTTP operation
*/
Mailbox.prototype.$update = function() {
var futureMailboxData;
futureMailboxData = Mailbox.$$resource.post(this.id, 'view', {sortingAttributes: {sort: 'date', asc: false}});
return this.$unwrap(futureMailboxData);
};
/**
* @function $loadMessage
* @memberof Mailbox.prototype
* @desc Check if the message is loaded and in any case, fetch more messages headers from the server.
* @returns true if the message metadata are already fetched
*/
Mailbox.prototype.$loadMessage = function(messageId) {
var startIndex = this.uidsMap[messageId],
endIndex,
max = this.$messages.length,
loaded = false,
uids,
futureHeadersData;
if (angular.isDefined(this.uidsMap[messageId]) && startIndex < this.$messages.length) {
// Index is valid
if (angular.isDefined(this.$messages[startIndex].subject)) {// || this.$messages[startIndex].loading) {
// Message headers are loaded or data is coming
loaded = true;
}
// Preload more headers if possible
endIndex = Math.min(startIndex + Mailbox.PRELOAD.LOOKAHEAD, max - 1);
if (!angular.isDefined(this.$messages[endIndex].subject)
&& !angular.isDefined(this.$messages[endIndex].loading)) {
endIndex = Math.min(startIndex + Mailbox.PRELOAD.SIZE, max);
for (uids = []; startIndex < endIndex && startIndex < max; startIndex++) {
if (angular.isDefined(this.$messages[startIndex].subject) || this.$messages[startIndex].loading) {
// Message at this index is already loaded; increase the end index
endIndex++;
}
else {
// Message at this index will be loaded
uids.push(this.$messages[startIndex].uid);
this.$messages[startIndex].loading = true;
}
}
Mailbox.$log.debug('Loading UIDs ' + uids.join(' '));
futureHeadersData = Mailbox.$$resource.post(this.id, 'headers', {uids: uids});
this.$unwrapHeaders(futureHeadersData);
}
}
return loaded;
};
/**
* @function $omit
* @memberof Mailbox.prototype
* @desc Return a sanitized object used to send to the server.
* @return an object literal copy of the Mailbox instance
*/
Mailbox.prototype.$omit = function() {
var mailbox = {};
angular.forEach(this, function(value, key) {
if (key != 'constructor' &&
key != 'children' &&
key[0] != '$') {
mailbox[key] = value;
}
});
return mailbox;
};
/**
* @function $unwrap
* @memberof Mailbox.prototype
* @desc Unwrap a promise and instanciate new Message objects using received data.
* @param {promise} futureMailboxData - a promise of the Mailbox's metadata
* @returns a promise of the HTTP operation
*/
Mailbox.prototype.$unwrap = function(futureMailboxData) {
var _this = this,
deferred = Mailbox.$q.defer();
this.$futureMailboxData = futureMailboxData;
this.$futureMailboxData.then(function(data) {
Mailbox.$timeout(function() {
var uids, headers;
angular.extend(_this, data);
_this.$messages = [];
_this.uidsMap = {};
if (_this.uids) {
// First entry of 'headers' are keys
headers = _.invoke(_this.headers[0], 'toLowerCase');
_this.headers.splice(0, 1);
// First entry of 'uids' are keys when threaded view is enabled
if (_this.threaded) {
uids = _this.uids[0];
_this.uids.splice(0, 1);
}
// Instanciate Message objects
_.reduce(_this.uids, function(msgs, msg, i) {
var data;
if (_this.threaded)
data = _.object(uids, msg);
else
data = {uid: msg.toString()};
// Build map of UID <=> index
_this.uidsMap[data.uid] = i;
msgs.push(new Mailbox.$Message(_this.accountId, _this.path, data));
return msgs;
}, _this.$messages);
// Extend Message objects with received headers
_.each(_this.headers, function(data) {
var msg = _.object(headers, data),
i = _this.uidsMap[msg.uid.toString()];
_.extend(_this.$messages[i], msg);
});
}
Mailbox.$log.debug('mailbox ' + _this.id + ' ready');
deferred.resolve(_this.$messages);
});
}, function(data) {
angular.extend(_this, data);
_this.isError = true;
deferred.reject();
});
return deferred.promise;
};
/**
* @function $unwrapHeaders
* @memberof Mailbox.prototype
* @desc Unwrap a promise and extend matching Message objects using received data.
* @param {promise} futureHeadersData - a promise of some messages metadata
*/
Mailbox.prototype.$unwrapHeaders = function(futureHeadersData) {
var _this = this;
futureHeadersData.then(function(data) {
Mailbox.$timeout(function() {
var headers, j;
if (data.length > 0) {
// First entry of 'headers' are keys
headers = _.invoke(data[0], 'toLowerCase');
data.splice(0, 1);
_.each(data, function(messageHeaders) {
messageHeaders = _.object(headers, messageHeaders);
j = _this.uidsMap[messageHeaders.uid.toString()];
if (angular.isDefined(j)) {
_.extend(_this.$messages[j], messageHeaders);
}
});
}
});
});
};
})();

View File

@@ -0,0 +1,116 @@
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
(function() {
'use strict';
/**
* @name Message
* @constructor
* @param {string} accountId - the account ID
* @param {string} mailboxPath - an array of the mailbox path components
* @param {object} futureAddressBookData - either an object literal or a promise
*/
function Message(accountId, mailboxPath, futureMessageData) {
this.accountId = accountId;
this.mailboxPath = mailboxPath;
// Data is immediately available
if (typeof futureMessageData.then !== 'function') {
//console.debug(JSON.stringify(futureMessageData, undefined, 2));
angular.extend(this, futureMessageData);
this.id = this.$absolutePath();
}
else {
// The promise will be unwrapped first
this.$unwrap(futureMessageData);
}
}
/**
* @memberof Message
* @desc The factory we'll use to register with Angular
* @returns the Message constructor
*/
Message.$factory = ['$q', '$timeout', '$log', '$sce', 'sgSettings', 'sgResource', function($q, $timeout, $log, $sce, Settings, Resource) {
angular.extend(Message, {
$q: $q,
$timeout: $timeout,
$log: $log,
$sce: $sce,
$$resource: new Resource(Settings.baseURL, Settings.activeUser)
});
return Message; // return constructor
}];
/* Factory registration in Angular module */
angular.module('SOGo.MailerUI')
.factory('sgMessage', Message.$factory);
/**
* @function $absolutePath
* @memberof Message.prototype
* @desc Build the path of the message
* @returns a string representing the path relative to the mail module
*/
Message.prototype.$absolutePath = function() {
var path;
path = _.map(this.mailboxPath.split('/'), function(component) {
return 'folder' + component.asCSSIdentifier();
});
path.splice(0, 0, this.accountId); // insert account ID
path.push(this.uid); // add message UID
return path.join('/');
};
/**
* @function $content
* @memberof Message.prototype
* @desc Fetch the message body along with other metadata such as the list of attachments.
* @returns the HTML representation of the body or a promise of the HTTP operation
*/
Message.prototype.$content = function() {
var futureMessageData;
if (this.$futureMessageData) {
return Message.$sce.trustAs('html', this.content);
}
futureMessageData = Message.$$resource.fetch(this.id, 'view');
return this.$unwrap(futureMessageData);
};
/**
* @function $unwrap
* @memberof Message.prototype
* @desc Unwrap a promise.
* @param {promise} futureMessageData - a promise of some of the Message's data
*/
Message.prototype.$unwrap = function(futureMessageData) {
var _this = this,
deferred = Message.$q.defer();
// Expose the promise
this.$futureMessageData = futureMessageData;
// Resolve the promise
this.$futureMessageData.then(function(data) {
// Calling $timeout will force Angular to refresh the view
Message.$timeout(function() {
angular.extend(_this, data);
_this.id = _this.$absolutePath();
deferred.resolve(_this.content);
});
}, function(data) {
angular.extend(_this, data);
_this.isError = true;
Message.$log.error(_this.error);
deferred.reject();
});
return deferred.promise;
};
})();

View File

@@ -0,0 +1,348 @@
@import "settings";
$module-color: #D04747; // red
$module-secondary-color: #501B1B; // darker red
$topbar-bg-color: $module-color;
$topbar-link-bg-active-hover: scale-color($module-secondary-color, $lightness: -14%);
$topbar-link-bg-active: $module-secondary-color;
$topbar-link-bg-hover: scale-color($module-color, $lightness: -14%);
@import "foundation";
@mixin off-canvas-list {
list-style-type: none;
padding:0;
margin:0;
/* display: table; */
width: 100%;
li {
/*
+-----------------------------------------------+
+ li |
|+------+-----------------------------+--------+|
|| i | form | span ||
|+------+-----------------------------+--------+|
+-----------------------------------------------+
*/
/* display: table-row; */
transition: background 300ms ease;
label {
display: block;
padding: $off-canvas-label-padding;
color: $off-canvas-label-color;
text-transform: $off-canvas-label-text-transform;
font-size: $off-canvas-label-font-size;
font-weight: $off-canvas-label-font-weight;
background: $off-canvas-label-bg;
border-top: $off-canvas-label-border-top;
border-bottom: $off-canvas-label-border-bottom;
margin: $off-canvas-label-margin;
}
.folder-container {
display: table;
width: 100%;
.folder-content {
display: table-row;
>* {
display: table-cell;
padding: $off-canvas-link-padding;
color: $off-canvas-link-color;
/* border-bottom: $off-canvas-link-border-bottom; */
//vertical-align: middle;
}
}
&:hover {
background: scale-color($tabbar-bg, $lightness: -30%);
}
}
>span {
//vertical-align: middle; // causes glitch when selecting row
border-left: 3px solid transparent;
//width: 2em;
}
.icon {
color: $off-canvas-label-color;
width: 1px;
}
form {
margin: 0;
padding-left: 0;
padding-right: 0;
* {
color: $off-canvas-link-color;
display: block;
width: 100%;
padding: 0;
//padding-left: $off-canvas-link-padding;
}
input {
border: 0;
color: #333 !important;
font-size: 1rem;
height: $off-canvas-link-padding/2+1rem;
margin: 0;
}
}
&._selected {
//background-color: #242424 !important;
//border: 0 !important;
>span {
border-left-color: $primary-color;
}
//* {
// color: #eee;
//}
}
&._loading {
>span {
border-left-color: $off-canvas-label-color;
}
}
}
}
.folders-list {
@include off-canvas-wrap();
ul {
@include off-canvas-list();
>li >ul {
margin-left: 10px;
}
}
}
$i: 1;
@while $i < 12 {
.folders-list ul li .folder-container .folder-content > .childLevel#{$i} { padding-left: 15px * $i; }
$i: $i + 1;
}
$total-columns: 13;
$column-gutter: 0;
#pageContent {
@include grid-row($behavior: nest);
#mailboxesList {
position: absolute;
overflow: auto;
overflow-x: hidden;
top: $topbar-height;
bottom: 0;
background-color: #333;
@include grid-column($columns:13); //, $collapse:true);
@media #{$medium-up} {
@include grid-column($columns:3);
}
}
#messagesList {
position: absolute;
overflow: auto;
overflow-x: hidden;
top: $topbar-height;
bottom: 0;
left: 23.07692%;
background-color: $f-dropdown-list-hover-bg;
@include grid-column($columns:13);
@media #{$medium-up} {
@include grid-column($columns:4);
}
ul {
margin: 0;
padding: 5px 0;
li {
list-style-type: none;
//border-bottom: $topbar-divider-border-bottom;
width: 100%;
height: 56px;
float: left;
clear: left;
//border: 2px solid #fff;
//-webkit-border-radius: 4px;
//-moz-border-radius: 4px;
//border-radius: 4px;
//transition: all 300ms ease;
background-color: $f-dropdown-list-hover-bg;
transition: background 300ms ease;
a {
display: block;
color: #666;
//border-bottom: 1px dotted #ddd;
//width: 100%;
//font-size: $table-row-font-size;
//line-height: $table-line-height;
line-height: rem-calc(24);
padding: $table-head-padding;
//padding: rem-calc(8 10 18);
//margin: 0 rem-calc(12);
.name {
margin: 0;
//font-size: $table-head-font-size;
//color: $table-head-font-color;
font-weight: $table-head-font-weight;
}
div {
overflow: hidden;
white-space: nowrap;
}
span {
font-weight: normal;
font-size: smaller;
}
}
&.unread {
a {
.name,
.subject {
color: $table-head-font-color;
/* font-weight: $table-head-font-weight; */
}
}
}
&:hover,
&:active {
background-color: $f-dropdown-list-hover-bg;
//background-color: scale-color($f-dropdown-list-hover-bg, $lightness: 28%);
background-color: #fff;
}
&._selected,
&._selected span {
//background-color: $module-light-color;
//background-color: $sub-nav-active-bg-hover;
//background-color: $f-dropdown-list-hover-bg;
background-color: $module-color;
background-color: #fff;
//color: $module-color;
//color: $module-secondary-color;
.name {
//color: #fff;
}
}
}
}
}
#messageView {
position: absolute;
top: $topbar-height;
bottom: 0;
left: 53.8461507692%;
overflow: auto;
overflow-x: hidden;
border-left: $topbar-divider-border-bottom;
padding: $table-head-padding;
padding-top: 0;
@include grid-column($columns:13);
@media #{$medium-up} {
@include grid-column($columns:6);
}
h1, h2, h3, h4, h5, h6 {
margin-left: rem-calc(12);
margin-top: 0;
}
h1 {
margin-bottom: 0;
}
.header {
background-color: $secondary-color;
padding-bottom: 0.2em;
h1, h6 {
color: #fff;
}
.label {
background-color: transparent;
border: 1px solid $primary-color;
border: 1px solid scale-color($primary-color, $lightness: 52%);
color: $primary-color;
color: scale-color($primary-color, $lightness: 52%);
}
label {
}
}
/*
+-----------------------------------------------+
+ .attr (13) |
|+-------------+-------------------------------+|
|| .key (4) | .value (9) ||
|+-------------+-------------------------------+|
+-----------------------------------------------+
*/
.attr {
@include grid-row($behavior:collapse);
.key {
@include grid-column($columns:4);
label {
color: #999;
margin-right: rem-calc(12);
}
}
.value {
@include grid-column($columns:9);
&.single {
@include grid-column($offset: 4, $columns:9);
}
div {
a {
color: #666;
margin-left: 0.2em;
&:hover {
color: #000;
}
}
}
}
}
.buttonsToolbar {
margin-bottom: rem-calc(12);
background-color: #eee;
border-top: 2px solid #ddd;
.button {
margin: 0.4em;
&.alert {
//display: $button-display;
color: #fff;
}
}
}
.mailer_mailcontent {
background-color: #fff;
padding: .5em;
overflow: auto;
.mailer_mailcontent TABLE {
table-layout: auto;
}
}
}
}
h1 {
font-weight: lighter;
}
.buttonsToolbar {
text-align: right;
.button {
margin-bottom: 0;
font-size: 1.0em;
}
.folders-list & {
background-color: $tabbar-bg;
background-color: $off-canvas-bg;
text-align: center;
.button {
background-color: $tabbar-bg;
font-size: 1.5em;
transition: color 300ms ease;
color: scale-color($tabbar-bg, $lightness: 52%);
&:focus, &:hover {
color: $primary-color;
color: #fff;
//background-color: scale-color($tabbar-bg, $lightness: 13%);
}
}
}
}