Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2532034
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
266 KB
Referenced Files
None
Subscribers
None
View Options
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc
index 99a4ec840..123bfda7f 100644
--- a/program/steps/mail/compose.inc
+++ b/program/steps/mail/compose.inc
@@ -1,1411 +1,1412 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Compose a new mail message with all headers and attachments |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
$COMPOSE_ID = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GET);
$COMPOSE = null;
if ($COMPOSE_ID && $_SESSION['compose_data_'.$COMPOSE_ID]) {
$COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID];
}
// give replicated session storage some time to synchronize
$retries = 0;
while ($COMPOSE_ID && !is_array($COMPOSE) && $RCMAIL->db->is_replicated() && $retries++ < 5) {
usleep(500000);
$RCMAIL->session->reload();
if ($_SESSION['compose_data_'.$COMPOSE_ID]) {
$COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID];
}
}
// Nothing below is called during message composition, only at "new/forward/reply/draft" initialization or
// if a compose-ID is given (i.e. when the compose step is opened in a new window/tab).
if (!is_array($COMPOSE)) {
// Infinite redirect prevention in case of broken session (#1487028)
if ($COMPOSE_ID) {
// if we know the message with specified ID was already sent
// we can ignore the error and compose a new message (#1490009)
if ($COMPOSE_ID != $_SESSION['last_compose_session']) {
rcube::raise_error(array('code' => 450), false, true);
}
}
$COMPOSE_ID = uniqid(mt_rand());
$params = rcube_utils::request2param(rcube_utils::INPUT_GET, 'task|action', true);
$_SESSION['compose_data_'.$COMPOSE_ID] = array(
'id' => $COMPOSE_ID,
'param' => $params,
'mailbox' => $params['mbox'] ?: $RCMAIL->storage->get_folder(),
);
$COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID];
rcmail_process_compose_params($COMPOSE);
// check if folder for saving sent messages exists and is subscribed (#1486802)
if ($sent_folder = $COMPOSE['param']['sent_mbox']) {
rcmail_sendmail::check_sent_folder($sent_folder, true);
}
// redirect to a unique URL with all parameters stored in session
$OUTPUT->redirect(array(
'_action' => 'compose',
'_id' => $COMPOSE['id'],
'_search' => $_REQUEST['_search'],
));
}
// add some labels to client
$OUTPUT->add_label('notuploadedwarning', 'savingmessage', 'siginserted', 'responseinserted',
'messagesaved', 'converting', 'editorwarning', 'discard',
'fileuploaderror', 'sendmessage', 'newresponse', 'responsename', 'responsetext', 'save',
'savingresponse', 'restoresavedcomposedata', 'restoremessage', 'delete', 'restore', 'ignore',
'selectimportfile', 'messageissent', 'loadingdata', 'nopubkeyfor', 'nopubkeyforsender',
'encryptnoattachments','encryptedsendialog','searchpubkeyservers', 'importpubkeys',
'encryptpubkeysfound', 'search', 'close', 'import', 'keyid', 'keylength', 'keyexpired',
'keyrevoked', 'keyimportsuccess', 'keyservererror', 'attaching', 'namex', 'attachmentrename'
);
$OUTPUT->set_pagetitle($RCMAIL->gettext('compose'));
$OUTPUT->set_env('compose_id', $COMPOSE['id']);
$OUTPUT->set_env('session_id', session_id());
$OUTPUT->set_env('mailbox', $RCMAIL->storage->get_folder());
$OUTPUT->set_env('top_posting', intval($RCMAIL->config->get('reply_mode')) > 0);
$OUTPUT->set_env('sig_below', $RCMAIL->config->get('sig_below'));
$OUTPUT->set_env('save_localstorage', (bool)$RCMAIL->config->get('compose_save_localstorage'));
$OUTPUT->set_env('is_sent', false);
$OUTPUT->set_env('mimetypes', rcmail_supported_mimetypes());
$OUTPUT->set_env('keyservers', $RCMAIL->config->keyservers());
$drafts_mbox = $RCMAIL->config->get('drafts_mbox');
$config_show_sig = $RCMAIL->config->get('show_sig', 1);
// add config parameters to client script
if (strlen($drafts_mbox)) {
$OUTPUT->set_env('drafts_mailbox', $drafts_mbox);
$OUTPUT->set_env('draft_autosave', $RCMAIL->config->get('draft_autosave'));
}
// default font for HTML editor
$font = rcmail::font_defs($RCMAIL->config->get('default_font'));
if ($font && !is_array($font)) {
$OUTPUT->set_env('default_font', $font);
}
// default font size for HTML editor
if ($font_size = $RCMAIL->config->get('default_font_size')) {
$OUTPUT->set_env('default_font_size', $font_size);
}
// get reference message and set compose mode
if ($msg_uid = $COMPOSE['param']['draft_uid']) {
$compose_mode = rcmail_sendmail::MODE_DRAFT;
$OUTPUT->set_env('draft_id', $msg_uid);
$RCMAIL->storage->set_folder($drafts_mbox);
}
else if ($msg_uid = $COMPOSE['param']['reply_uid']) {
$compose_mode = rcmail_sendmail::MODE_REPLY;
}
else if ($msg_uid = $COMPOSE['param']['forward_uid']) {
$compose_mode = rcmail_sendmail::MODE_FORWARD;
$COMPOSE['forward_uid'] = $msg_uid;
$COMPOSE['as_attachment'] = !empty($COMPOSE['param']['attachment']);
}
else if ($msg_uid = $COMPOSE['param']['uid']) {
$compose_mode = rcmail_sendmail::MODE_EDIT;
}
if ($compose_mode) {
$COMPOSE['mode'] = $compose_mode;
$OUTPUT->set_env('compose_mode', $compose_mode);
}
if ($compose_mode == rcmail_sendmail::MODE_EDIT || $compose_mode == rcmail_sendmail::MODE_DRAFT) {
// don't add signature in draft/edit mode, we'll also not remove the old-one
// but only on page display, later we should be able to change identity/sig (#1489229)
if ($config_show_sig == 1 || $config_show_sig == 2) {
$OUTPUT->set_env('show_sig_later', true);
}
}
else if ($config_show_sig == 1)
$OUTPUT->set_env('show_sig', true);
else if ($config_show_sig == 2 && empty($compose_mode))
$OUTPUT->set_env('show_sig', true);
else if ($config_show_sig == 3 && ($compose_mode == rcmail_sendmail::MODE_REPLY || $compose_mode == rcmail_sendmail::MODE_FORWARD))
$OUTPUT->set_env('show_sig', true);
// set line length for body wrapping
$LINE_LENGTH = $RCMAIL->config->get('line_length', 72);
if (!empty($msg_uid) && empty($COMPOSE['as_attachment'])) {
$mbox_name = $RCMAIL->storage->get_folder();
// set format before rcube_message construction
// use the same format as for the message view
if (isset($_SESSION['msg_formats'][$mbox_name.':'.$msg_uid])) {
$RCMAIL->config->set('prefer_html', $_SESSION['msg_formats'][$mbox_name.':'.$msg_uid]);
}
else {
$prefer_html = $RCMAIL->config->get('prefer_html') || $RCMAIL->config->get('htmleditor')
|| $compose_mode == rcmail_sendmail::MODE_DRAFT || $compose_mode == rcmail_sendmail::MODE_EDIT;
$RCMAIL->config->set('prefer_html', $prefer_html);
}
$MESSAGE = new rcube_message($msg_uid);
// make sure message is marked as read
if ($MESSAGE->headers && $MESSAGE->context === null && empty($MESSAGE->headers->flags['SEEN'])) {
$RCMAIL->storage->set_flag($msg_uid, 'SEEN');
}
if (!empty($MESSAGE->headers->charset)) {
$RCMAIL->storage->set_charset($MESSAGE->headers->charset);
}
if (!$MESSAGE->headers) {
// error
}
else if ($compose_mode == rcmail_sendmail::MODE_FORWARD || $compose_mode == rcmail_sendmail::MODE_REPLY) {
if ($compose_mode == rcmail_sendmail::MODE_REPLY) {
$COMPOSE['reply_uid'] = $MESSAGE->context === null ? $msg_uid : null;
if (!empty($COMPOSE['param']['all'])) {
$MESSAGE->reply_all = $COMPOSE['param']['all'];
}
}
else {
$COMPOSE['forward_uid'] = $msg_uid;
}
$COMPOSE['reply_msgid'] = $MESSAGE->headers->messageID;
$COMPOSE['references'] = trim($MESSAGE->headers->references . " " . $MESSAGE->headers->messageID);
// Save the sent message in the same folder of the message being replied to
if ($RCMAIL->config->get('reply_same_folder') && ($sent_folder = $COMPOSE['mailbox'])
&& rcmail_sendmail::check_sent_folder($sent_folder, false)
) {
$COMPOSE['param']['sent_mbox'] = $sent_folder;
}
}
else if ($compose_mode == rcmail_sendmail::MODE_DRAFT || $compose_mode == rcmail_sendmail::MODE_EDIT) {
if ($compose_mode == rcmail_sendmail::MODE_DRAFT) {
if ($draft_info = $MESSAGE->headers->get('x-draft-info')) {
// get reply_uid/forward_uid to flag the original message when sending
$info = rcmail_sendmail::draftinfo_decode($draft_info);
if ($info['type'] == 'reply')
$COMPOSE['reply_uid'] = $info['uid'];
else if ($info['type'] == 'forward')
$COMPOSE['forward_uid'] = $info['uid'];
$COMPOSE['mailbox'] = $info['folder'];
// Save the sent message in the same folder of the message being replied to
if ($RCMAIL->config->get('reply_same_folder') && ($sent_folder = $info['folder'])
&& rcmail_sendmail::check_sent_folder($sent_folder, false)
) {
$COMPOSE['param']['sent_mbox'] = $sent_folder;
}
}
if (($msgid = $MESSAGE->headers->get('message-id')) && !preg_match('/^mid:[0-9]+$/', $msgid)) {
$COMPOSE['param']['message-id'] = $msgid;
}
// use message UID as draft_id
$OUTPUT->set_env('draft_id', $msg_uid);
}
if ($in_reply_to = $MESSAGE->headers->get('in-reply-to')) {
$COMPOSE['reply_msgid'] = '<' . $in_reply_to . '>';
}
$COMPOSE['references'] = $MESSAGE->headers->references;
}
}
else {
$MESSAGE = new stdClass();
// apply mailto: URL parameters
if (!empty($COMPOSE['param']['in-reply-to'])) {
$COMPOSE['reply_msgid'] = '<' . $COMPOSE['param']['in-reply-to'] . '>';
}
if (!empty($COMPOSE['param']['references'])) {
$COMPOSE['references'] = $COMPOSE['param']['references'];
}
}
if (!empty($COMPOSE['reply_msgid'])) {
$OUTPUT->set_env('reply_msgid', $COMPOSE['reply_msgid']);
}
// Initialize helper class to build the UI
$SENDMAIL = new rcmail_sendmail($COMPOSE, array('message' => $MESSAGE));
// process $MESSAGE body/attachments, set $MESSAGE_BODY/$HTML_MODE vars and some session data
$MESSAGE_BODY = rcmail_prepare_message_body();
// register UI objects (Note: some objects are registered by rcmail_sendmail above)
$OUTPUT->add_handlers(array(
'composebody' => 'rcmail_compose_body',
'composeattachmentlist' => 'rcmail_compose_attachment_list',
'composeattachmentform' => 'rcmail_compose_attachment_form',
'composeattachment' => 'rcmail_compose_attachment_field',
'filedroparea' => 'rcmail_compose_file_drop_area',
'editorselector' => 'rcmail_editor_selector',
'addressbooks' => 'rcmail_addressbook_list',
'addresslist' => 'rcmail_contacts_list',
'responseslist' => 'rcmail_compose_responses_list',
));
$OUTPUT->include_script('publickey.js');
rcmail_spellchecker_init();
$OUTPUT->send('compose');
/****** compose mode functions ********/
// process compose request parameters
function rcmail_process_compose_params(&$COMPOSE)
{
if ($COMPOSE['param']['to']) {
$mailto = explode('?', $COMPOSE['param']['to'], 2);
// #1486037: remove "mailto:" prefix
$COMPOSE['param']['to'] = preg_replace('/^mailto:/i', '', $mailto[0]);
// #1490346: decode the recipient address
// #1490510: use raw encoding for correct "+" character handling as specified in RFC6068
$COMPOSE['param']['to'] = rawurldecode($COMPOSE['param']['to']);
// Supported case-insensitive tokens in mailto URL
$url_tokens = array('to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'body');
if (!empty($mailto[1])) {
parse_str($mailto[1], $query);
foreach ($query as $f => $val) {
if (($key = array_search(strtolower($f), $url_tokens)) !== false) {
$f = $url_tokens[$key];
}
// merge mailto: addresses with addresses from 'to' parameter
if ($f == 'to' && !empty($COMPOSE['param']['to'])) {
$to_addresses = rcube_mime::decode_address_list($COMPOSE['param']['to'], null, true, null, true);
$add_addresses = rcube_mime::decode_address_list($val, null, true);
foreach ($add_addresses as $addr) {
if (!in_array($addr['mailto'], $to_addresses)) {
$to_addresses[] = $addr['mailto'];
$COMPOSE['param']['to'] = (!empty($to_addresses) ? ', ' : '') . $addr['string'];
}
}
}
else {
$COMPOSE['param'][$f] = $val;
}
}
}
}
// resolve _forward_uid=* to an absolute list of messages from a search result
if ($COMPOSE['param']['forward_uid'] == '*' && is_object($_SESSION['search'][1])) {
$COMPOSE['param']['forward_uid'] = $_SESSION['search'][1]->get();
}
// clean HTML message body which can be submitted by URL
if (!empty($COMPOSE['param']['body'])) {
if ($COMPOSE['param']['html'] = strpos($COMPOSE['param']['body'], '<') !== false) {
$wash_params = array('safe' => false, 'inline_html' => true);
$COMPOSE['param']['body'] = rcmail_prepare_html_body($COMPOSE['param']['body'], $wash_params);
}
}
$RCMAIL = rcmail::get_instance();
// select folder where to save the sent message
$COMPOSE['param']['sent_mbox'] = $RCMAIL->config->get('sent_mbox');
// pipe compose parameters thru plugins
$plugin = $RCMAIL->plugins->exec_hook('message_compose', $COMPOSE);
$COMPOSE['param'] = array_merge($COMPOSE['param'], $plugin['param']);
// add attachments listed by message_compose hook
if (is_array($plugin['attachments'])) {
foreach ($plugin['attachments'] as $attach) {
// we have structured data
if (is_array($attach)) {
$attachment = $attach + array('group' => $COMPOSE_ID);
}
// only a file path is given
else {
$filename = basename($attach);
$attachment = array(
'group' => $COMPOSE_ID,
'name' => $filename,
'mimetype' => rcube_mime::file_content_type($attach, $filename),
'size' => filesize($attach),
'path' => $attach,
);
}
// save attachment if valid
if (($attachment['data'] && $attachment['name']) || ($attachment['path'] && file_exists($attachment['path']))) {
$attachment = rcmail::get_instance()->plugins->exec_hook('attachment_save', $attachment);
}
if ($attachment['status'] && !$attachment['abort']) {
unset($attachment['data'], $attachment['status'], $attachment['abort']);
$COMPOSE['attachments'][$attachment['id']] = $attachment;
}
}
}
}
function rcmail_compose_editor_mode()
{
global $RCMAIL, $COMPOSE;
static $useHtml;
if ($useHtml !== null) {
return $useHtml;
}
$html_editor = intval($RCMAIL->config->get('htmleditor'));
$compose_mode = $COMPOSE['mode'];
if (is_bool($COMPOSE['param']['html'])) {
$useHtml = $COMPOSE['param']['html'];
}
else if (isset($_POST['_is_html'])) {
$useHtml = !empty($_POST['_is_html']);
}
else if ($compose_mode == rcmail_sendmail::MODE_DRAFT || $compose_mode == rcmail_sendmail::MODE_EDIT) {
$useHtml = rcmail_message_is_html();
}
else if ($compose_mode == rcmail_sendmail::MODE_REPLY) {
$useHtml = $html_editor == 1 || ($html_editor >= 2 && rcmail_message_is_html());
}
else if ($compose_mode == rcmail_sendmail::MODE_FORWARD) {
$useHtml = $html_editor == 1 || $html_editor == 4
|| ($html_editor == 3 && rcmail_message_is_html());
}
else {
$useHtml = $html_editor == 1 || $html_editor == 4;
}
return $useHtml;
}
function rcmail_message_is_html()
{
global $RCMAIL, $MESSAGE;
return $RCMAIL->config->get('prefer_html') && ($MESSAGE instanceof rcube_message) && $MESSAGE->has_html_part(true);
}
function rcmail_spellchecker_init()
{
global $RCMAIL, $OUTPUT;
// Set language list
if ($RCMAIL->config->get('enable_spellcheck')) {
$spellchecker = new rcube_spellchecker();
$spellcheck_langs = $spellchecker->languages();
}
if (!empty($spellchecker) && empty($spellcheck_langs)) {
if ($err = $spellchecker->error()) {
rcube::raise_error(array('code' => 500,
'file' => __FILE__, 'line' => __LINE__,
'message' => "Spell check engine error: " . trim($err)),
true, false);
}
}
else if (!empty($spellchecker)) {
$dictionary = (bool) $RCMAIL->config->get('spellcheck_dictionary');
$lang = $_SESSION['language'];
// if not found in the list, try with two-letter code
if (!$spellcheck_langs[$lang]) {
$lang = strtolower(substr($lang, 0, 2));
}
if (!$spellcheck_langs[$lang]) {
$lang = 'en';
}
$editor_lang_set = array();
foreach ($spellcheck_langs as $key => $name) {
$editor_lang_set[] = ($key == $lang ? '+' : '') . rcube::JQ($name).'='.rcube::JQ($key);
}
// include GoogieSpell
$OUTPUT->include_script('googiespell.js');
$OUTPUT->add_script(sprintf(
"var googie = new GoogieSpell('%s/images/googiespell/','%s&lang=', %s);\n".
"googie.lang_chck_spell = \"%s\";\n".
"googie.lang_rsm_edt = \"%s\";\n".
"googie.lang_close = \"%s\";\n".
"googie.lang_revert = \"%s\";\n".
"googie.lang_no_error_found = \"%s\";\n".
"googie.lang_learn_word = \"%s\";\n".
"googie.setLanguages(%s);\n".
"googie.setCurrentLanguage('%s');\n".
"googie.setDecoration(false);\n".
"googie.decorateTextarea(rcmail.env.composebody);\n",
$RCMAIL->output->asset_url($RCMAIL->output->get_skin_path()),
$RCMAIL->url(array('_task' => 'utils', '_action' => 'spell', '_remote' => 1)),
!empty($dictionary) ? 'true' : 'false',
rcube::JQ(rcube::Q($RCMAIL->gettext('checkspelling'))),
rcube::JQ(rcube::Q($RCMAIL->gettext('resumeediting'))),
rcube::JQ(rcube::Q($RCMAIL->gettext('close'))),
rcube::JQ(rcube::Q($RCMAIL->gettext('revertto'))),
rcube::JQ(rcube::Q($RCMAIL->gettext('nospellerrors'))),
rcube::JQ(rcube::Q($RCMAIL->gettext('addtodict'))),
rcube_output::json_serialize($spellcheck_langs),
$lang
), 'foot');
$OUTPUT->add_label('checking');
$OUTPUT->set_env('spellcheck_langs', join(',', $editor_lang_set));
$OUTPUT->set_env('spell_langs', $spellcheck_langs);
$OUTPUT->set_env('spell_lang', $lang);
}
}
function rcmail_prepare_message_body()
{
global $RCMAIL, $MESSAGE, $COMPOSE, $HTML_MODE, $CID_MAP;
$CID_MAP = array();
// use posted message body
if (!empty($_POST['_message'])) {
$body = rcube_utils::get_input_value('_message', rcube_utils::INPUT_POST, true);
$isHtml = (bool) rcube_utils::get_input_value('_is_html', rcube_utils::INPUT_POST);
}
else if ($COMPOSE['param']['body']) {
$body = $COMPOSE['param']['body'];
$isHtml = (bool) $COMPOSE['param']['html'];
}
// forward as attachment
else if ($COMPOSE['mode'] == rcmail_sendmail::MODE_FORWARD && $COMPOSE['as_attachment']) {
$isHtml = rcmail_compose_editor_mode();
$body = '';
rcmail_write_forward_attachments();
}
// reply/edit/draft/forward
else if ($COMPOSE['mode'] && ($COMPOSE['mode'] != rcmail_sendmail::MODE_REPLY || intval($RCMAIL->config->get('reply_mode')) != -1)) {
$isHtml = rcmail_compose_editor_mode();
$messages = array();
// save inline images to files (before HTML body washing)
if ($COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY) {
rcmail_write_inline_attachments($MESSAGE);
}
// save attachments to files (before HTML body washing)
else {
rcmail_write_compose_attachments($MESSAGE, $isHtml);
}
// set is_safe flag (before HTML body washing)
if ($COMPOSE['mode'] == rcmail_sendmail::MODE_DRAFT) {
$MESSAGE->is_safe = true;
}
else {
rcmail_check_safe($MESSAGE);
}
if (!empty($MESSAGE->parts)) {
// collect IDs of message/rfc822 parts
foreach ($MESSAGE->mime_parts() as $part) {
if ($part->mimetype == 'message/rfc822') {
$messages[] = $part->mime_id;
}
}
foreach ($MESSAGE->parts as $part) {
if ($part->realtype == 'multipart/encrypted') {
// find the encrypted message payload part
if ($pgp_mime_part = $MESSAGE->get_multipart_encrypted_part()) {
$RCMAIL->output->set_env('pgp_mime_message', array(
'_mbox' => $RCMAIL->storage->get_folder(),
'_uid' => $MESSAGE->uid,
'_part' => $pgp_mime_part->mime_id,
));
}
continue;
}
// skip no-content and attachment parts (#1488557)
if ($part->type != 'content' || !$part->size || $MESSAGE->is_attachment($part)) {
continue;
}
// skip all content parts inside the message/rfc822 part
foreach ($messages as $mimeid) {
if (strpos($part->mime_id, $mimeid . '.') === 0) {
continue 2;
}
}
if ($part_body = rcmail_compose_part_body($part, $isHtml)) {
$body .= ($body ? ($isHtml ? '<br/>' : "\n") : '') . $part_body;
}
}
}
else {
$body = rcmail_compose_part_body($MESSAGE, $isHtml);
}
// compose reply-body
if ($COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY) {
$body = rcmail_create_reply_body($body, $isHtml);
if ($MESSAGE->pgp_mime) {
$RCMAIL->output->set_env('compose_reply_header', rcmail_get_reply_header($MESSAGE));
}
}
// forward message body inline
else if ($COMPOSE['mode'] == rcmail_sendmail::MODE_FORWARD) {
$body = rcmail_create_forward_body($body, $isHtml);
}
// load draft message body
else if ($COMPOSE['mode'] == rcmail_sendmail::MODE_DRAFT || $COMPOSE['mode'] == rcmail_sendmail::MODE_EDIT) {
$body = rcmail_create_draft_body($body, $isHtml);
}
}
// new message
else {
$isHtml = rcmail_compose_editor_mode();
}
$plugin = $RCMAIL->plugins->exec_hook('message_compose_body',
array('body' => $body, 'html' => $isHtml, 'mode' => $COMPOSE['mode']));
$body = $plugin['body'];
unset($plugin);
// add blocked.gif attachment (#1486516)
$regexp = '/ src="' . preg_quote($RCMAIL->output->asset_url('program/resources/blocked.gif'), '/') . '"/';
if ($isHtml && preg_match($regexp, $body)) {
$content = $RCMAIL->get_resource_content('blocked.gif');
if ($content && ($attachment = rcmail_save_image('blocked.gif', 'image/gif', $content))) {
$COMPOSE['attachments'][$attachment['id']] = $attachment;
$url = sprintf('%s&_id=%s&_action=display-attachment&_file=rcmfile%s',
$RCMAIL->comm_path, $COMPOSE['id'], $attachment['id']);
$body = preg_replace($regexp, ' src="' . $url . '"', $body);
}
}
$HTML_MODE = $isHtml;
return $body;
}
function rcmail_compose_part_body($part, $isHtml = false)
{
global $RCMAIL, $COMPOSE, $MESSAGE, $LINE_LENGTH;
// Check if we have enough memory to handle the message in it
// #1487424: we need up to 10x more memory than the body
if (!rcube_utils::mem_check($part->size * 10)) {
return '';
}
// fetch part if not available
$body = $MESSAGE->get_part_body($part->mime_id, true);
// message is cached but not exists (#1485443), or other error
if ($body === false) {
return '';
}
// register this part as pgp encrypted
if (strpos($body, '-----BEGIN PGP MESSAGE-----') !== false) {
$MESSAGE->pgp_mime = true;
$RCMAIL->output->set_env('pgp_mime_message', array(
'_mbox' => $RCMAIL->storage->get_folder(), '_uid' => $MESSAGE->uid, '_part' => $part->mime_id,
));
}
if ($isHtml) {
if ($part->ctype_secondary == 'html') {
$body = rcmail_prepare_html_body($body);
}
else if ($part->ctype_secondary == 'enriched') {
$body = rcube_enriched::to_html($body);
}
else {
// try to remove the signature
if ($COMPOSE['mode'] != rcmail_sendmail::MODE_DRAFT && $COMPOSE['mode'] != rcmail_sendmail::MODE_EDIT) {
if ($RCMAIL->config->get('strip_existing_sig', true)) {
$body = rcmail_remove_signature($body);
}
}
// add HTML formatting
$body = rcmail_plain_body($body, $part->ctype_parameters['format'] == 'flowed', $part->ctype_parameters['delsp'] == 'yes');
}
}
else {
if ($part->ctype_secondary == 'enriched') {
$body = rcube_enriched::to_html($body);
$part->ctype_secondary = 'html';
}
if ($part->ctype_secondary == 'html') {
// use html part if it has been used for message (pre)viewing
// decrease line length for quoting
$len = $COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY ? $LINE_LENGTH-2 : $LINE_LENGTH;
$body = $RCMAIL->html2text($body, array('width' => $len));
}
else {
if ($part->ctype_secondary == 'plain' && $part->ctype_parameters['format'] == 'flowed') {
$body = rcube_mime::unfold_flowed($body, null, $part->ctype_parameters['delsp'] == 'yes');
}
// try to remove the signature
if ($COMPOSE['mode'] != rcmail_sendmail::MODE_DRAFT && $COMPOSE['mode'] != rcmail_sendmail::MODE_EDIT) {
if ($RCMAIL->config->get('strip_existing_sig', true)) {
$body = rcmail_remove_signature($body);
}
}
}
}
return $body;
}
function rcmail_compose_body($attrib)
{
global $RCMAIL, $OUTPUT, $HTML_MODE, $MESSAGE_BODY, $SENDMAIL;
list($form_start, $form_end) = $SENDMAIL->form_tags($attrib);
unset($attrib['form']);
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmComposeBody';
}
// If desired, set this textarea to be editable by TinyMCE
$attrib['data-html-editor'] = true;
if ($HTML_MODE) {
$attrib['class'] = trim($attrib['class'] . ' mce_editor');
}
$attrib['name'] = '_message';
$textarea = new html_textarea($attrib);
$hidden = new html_hiddenfield();
$hidden->add(array('name' => '_draft_saveid', 'value' => $RCMAIL->output->get_env('draft_id')));
$hidden->add(array('name' => '_draft', 'value' => ''));
$hidden->add(array('name' => '_is_html', 'value' => $HTML_MODE ? "1" : "0"));
$hidden->add(array('name' => '_framed', 'value' => '1'));
$OUTPUT->set_env('composebody', $attrib['id']);
// include HTML editor
$RCMAIL->html_editor();
return ($form_start ? "$form_start\n" : '')
. "\n" . $hidden->show() . "\n" . $textarea->show($MESSAGE_BODY)
. ($form_end ? "\n$form_end\n" : '');
}
function rcmail_create_reply_body($body, $bodyIsHtml)
{
global $RCMAIL, $MESSAGE, $LINE_LENGTH;
$reply_mode = (int) $RCMAIL->config->get('reply_mode');
$reply_indent = $reply_mode != 2;
// In top-posting without quoting it's better to use multi-line header
if ($reply_mode == 2) {
$prefix = rcmail_get_forward_header($MESSAGE, $bodyIsHtml, false);
}
else {
$prefix = rcmail_get_reply_header($MESSAGE);
if ($bodyIsHtml) {
$prefix = '<p id="reply-intro">' . rcube::Q($prefix) . '</p>';
}
else {
$prefix .= "\n";
}
}
if (!$bodyIsHtml) {
// soft-wrap and quote message text
$body = rcmail_wrap_and_quote($body, $LINE_LENGTH, $reply_indent);
if ($reply_mode > 0) { // top-posting
$prefix = "\n\n\n" . $prefix;
$suffix = '';
}
else {
$suffix = "\n";
}
}
else {
$suffix = '';
if ($reply_indent) {
$prefix .= '<blockquote>';
$suffix .= '</blockquote>';
}
if ($reply_mode == 2) {
// top-posting, no indent
}
else if ($reply_mode > 0) {
// top-posting
$prefix = '<br>' . $prefix;
}
else {
$suffix .= '<p><br/></p>';
}
}
return $prefix . $body . $suffix;
}
function rcmail_get_reply_header($message)
{
global $RCMAIL;
$from = array_pop(rcube_mime::decode_address_list($message->get_header('from'), 1, false, $message->headers->charset));
return $RCMAIL->gettext(array(
'name' => 'mailreplyintro',
'vars' => array(
'date' => $RCMAIL->format_date($message->headers->date, $RCMAIL->config->get('date_long')),
'sender' => $from['name'] ?: rcube_utils::idn_to_utf8($from['mailto']),
)
));
}
function rcmail_create_forward_body($body, $bodyIsHtml)
{
global $MESSAGE;
return rcmail_get_forward_header($MESSAGE, $bodyIsHtml) . trim($body, "\r\n");
}
function rcmail_get_forward_header($message, $bodyIsHtml = false, $extended = true)
{
global $RCMAIL;
$date = $RCMAIL->format_date($message->headers->date, $RCMAIL->config->get('date_long'));
if (!$bodyIsHtml) {
$prefix = "\n\n\n-------- " . $RCMAIL->gettext('originalmessage') . " --------\n";
$prefix .= $RCMAIL->gettext('subject') . ': ' . $message->subject . "\n";
$prefix .= $RCMAIL->gettext('date') . ': ' . $date . "\n";
$prefix .= $RCMAIL->gettext('from') . ': ' . $message->get_header('from') . "\n";
$prefix .= $RCMAIL->gettext('to') . ': ' . $message->get_header('to') . "\n";
if ($extended && ($cc = $message->headers->get('cc'))) {
$prefix .= $RCMAIL->gettext('cc') . ': ' . $cc . "\n";
}
if ($extended && ($replyto = $message->headers->get('reply-to')) && $replyto != $message->get_header('from')) {
$prefix .= $RCMAIL->gettext('replyto') . ': ' . $replyto . "\n";
}
$prefix .= "\n";
}
else {
$prefix = sprintf(
"<br /><p>-------- " . $RCMAIL->gettext('originalmessage') . " --------</p>" .
"<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tbody>" .
"<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>" .
"<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>" .
"<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>" .
"<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>",
$RCMAIL->gettext('subject'), rcube::Q($message->subject),
$RCMAIL->gettext('date'), rcube::Q($date),
$RCMAIL->gettext('from'), rcube::Q($message->get_header('from'), 'replace'),
$RCMAIL->gettext('to'), rcube::Q($message->get_header('to'), 'replace'));
if ($extended && ($cc = $message->headers->get('cc'))) {
$prefix .= sprintf("<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>",
$RCMAIL->gettext('cc'), rcube::Q($cc, 'replace'));
}
if ($extended && ($replyto = $message->headers->get('reply-to')) && $replyto != $message->get_header('from')) {
$prefix .= sprintf("<tr><th align=\"right\" nowrap=\"nowrap\" valign=\"baseline\">%s: </th><td>%s</td></tr>",
$RCMAIL->gettext('replyto'), rcube::Q($replyto, 'replace'));
}
$prefix .= "</tbody></table><br>";
}
return $prefix;
}
function rcmail_create_draft_body($body, $bodyIsHtml)
{
// Return the draft body as-is
return $body;
}
// Clean up HTML content of Draft/Reply/Forward (part of the message)
function rcmail_prepare_html_body($body, $wash_params = array())
{
global $CID_MAP, $MESSAGE, $COMPOSE;
static $part_no;
// Set attributes of the part container
$container_id = $COMPOSE['mode'] . 'body' . (++$part_no);
$container_attrib = array('id' => $container_id);
$body_args = array(
'safe' => $MESSAGE->is_safe,
'plain' => false,
'css_prefix' => 'v' . $part_no,
);
// remove comments (produced by washtml)
$replace = array('/<!--[^>]+-->/' => '');
if ($COMPOSE['mode'] == rcmail_sendmail::MODE_DRAFT) {
// convert TinyMCE's empty-line sequence (#1490463)
$replace['/<p>\xC2\xA0<\/p>/'] = '<p><br /></p>';
// remove <body> tags
$replace['/<body([^>]*)>/i'] = '';
$replace['/<\/body>/i'] = '';
}
else {
$body_args['container_id'] = $container_id;
$body_args['container_attrib'] = $container_attrib;
}
// Make the HTML content safe and clean
$body = rcmail_wash_html($body, $wash_params + $body_args, $CID_MAP);
$body = preg_replace(array_keys($replace), array_values($replace), $body);
$body = rcmail_html4inline($body, $body_args);
if ($COMPOSE['mode'] != rcmail_sendmail::MODE_DRAFT) {
$body = html::div($container_attrib, $body);
}
return $body;
}
// Removes signature from the message body
function rcmail_remove_signature($body)
{
global $RCMAIL;
$body = str_replace("\r\n", "\n", $body);
$len = strlen($body);
$sig_max_lines = $RCMAIL->config->get('sig_max_lines', 15);
while (($sp = strrpos($body, "-- \n", $sp ? -$len+$sp-1 : 0)) !== false) {
if ($sp == 0 || $body[$sp-1] == "\n") {
// do not touch blocks with more that X lines
if (substr_count($body, "\n", $sp) < $sig_max_lines) {
$body = substr($body, 0, max(0, $sp-1));
}
break;
}
}
return $body;
}
function rcmail_write_compose_attachments(&$message, $bodyIsHtml)
{
global $RCMAIL, $COMPOSE, $CID_MAP;
if ($message->pgp_mime || !empty($COMPOSE['forward_attachments'])) {
return $CID_MAP;
}
$messages = array();
$loaded_attachments = array();
foreach ((array)$COMPOSE['attachments'] as $attachment) {
$loaded_attachments[$attachment['name'] . $attachment['mimetype']] = $attachment;
}
foreach ((array) $message->mime_parts() as $pid => $part) {
if ($part->mimetype == 'message/rfc822') {
$messages[] = $part->mime_id;
}
if ($part->disposition == 'attachment' || ($part->disposition == 'inline' && $bodyIsHtml) || $part->filename) {
// skip parts that aren't valid attachments
if ($part->ctype_primary == 'multipart' || $part->mimetype == 'application/ms-tnef') {
continue;
}
// skip message attachments in reply mode
if ($part->ctype_primary == 'message' && $COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY) {
continue;
}
// skip inline images when forwarding in text mode
if ($part->content_id && $part->disposition == 'inline' && !$bodyIsHtml && $COMPOSE['mode'] == rcmail_sendmail::MODE_FORWARD) {
continue;
}
// skip version.txt parts of multipart/encrypted messages
if ($message->pgp_mime && $part->mimetype == 'application/pgp-encrypted' && $part->filename == 'version.txt') {
continue;
}
// skip attachments included in message/rfc822 attachment (#1486487, #1490607)
foreach ($messages as $mimeid) {
if (strpos($part->mime_id, $mimeid . '.') === 0) {
continue 2;
}
}
if (($attachment = $loaded_attachments[rcmail_attachment_name($part) . $part->mimetype])
|| ($attachment = rcmail_save_attachment($message, $pid, $COMPOSE['id']))
) {
if ($bodyIsHtml && ($part->content_id || $part->content_location)) {
$url = sprintf('%s&_id=%s&_action=display-attachment&_file=rcmfile%s',
$RCMAIL->comm_path, $COMPOSE['id'], $attachment['id']);
if ($part->content_id)
$CID_MAP['cid:'.$part->content_id] = $url;
else
$CID_MAP[$part->content_location] = $url;
}
}
}
}
$COMPOSE['forward_attachments'] = true;
return $CID_MAP;
}
function rcmail_write_inline_attachments(&$message)
{
global $RCMAIL, $COMPOSE, $CID_MAP;
if ($message->pgp_mime) {
return $CID_MAP;
}
$messages = array();
foreach ((array) $message->mime_parts() as $pid => $part) {
if ($part->mimetype == 'message/rfc822') {
$messages[] = $part->mime_id;
}
if (($part->content_id || $part->content_location) && $part->filename) {
// skip attachments included in message/rfc822 attachment (#1486487, #1490607)
foreach ($messages as $mimeid) {
if (strpos($part->mime_id, $mimeid . '.') === 0) {
continue 2;
}
}
if ($attachment = rcmail_save_attachment($message, $pid, $COMPOSE['id'])) {
$url = sprintf('%s&_id=%s&_action=display-attachment&_file=rcmfile%s',
$RCMAIL->comm_path, $COMPOSE['id'], $attachment['id']);
if ($part->content_id)
$CID_MAP['cid:'.$part->content_id] = $url;
else
$CID_MAP[$part->content_location] = $url;
}
}
}
return $CID_MAP;
}
// Creates attachment(s) from the forwarded message(s)
function rcmail_write_forward_attachments()
{
global $RCMAIL, $COMPOSE, $MESSAGE;
if ($MESSAGE->pgp_mime) {
return;
}
$storage = $RCMAIL->get_storage();
$names = array();
$refs = array();
$size_errors = 0;
$size_limit = parse_bytes($RCMAIL->config->get('max_message_size'));
$total_size = 10 * 1024; // size of message body, to start with
$loaded_attachments = array();
foreach ((array)$COMPOSE['attachments'] as $attachment) {
$loaded_attachments[$attachment['name'] . $attachment['mimetype']] = $attachment;
$total_size += $attachment['size'];
}
if ($COMPOSE['forward_uid'] == '*') {
$index = $storage->index(null, rcmail_sort_column(), rcmail_sort_order());
$COMPOSE['forward_uid'] = $index->get();
}
else if (!is_array($COMPOSE['forward_uid']) && strpos($COMPOSE['forward_uid'], ':')) {
$COMPOSE['forward_uid'] = rcube_imap_generic::uncompressMessageSet($COMPOSE['forward_uid']);
}
else if (is_string($COMPOSE['forward_uid'])) {
$COMPOSE['forward_uid'] = explode(',', $COMPOSE['forward_uid']);
}
foreach ((array)$COMPOSE['forward_uid'] as $uid) {
$message = new rcube_message($uid);
if (empty($message->headers)) {
continue;
}
if (!empty($message->headers->charset)) {
$storage->set_charset($message->headers->charset);
}
if (empty($MESSAGE->subject)) {
$MESSAGE->subject = $message->subject;
}
// generate (unique) attachment name
$name = strlen($message->subject) ? mb_substr($message->subject, 0, 64) : 'message_rfc822';
if (!empty($names[$name])) {
$names[$name]++;
$name .= '_' . $names[$name];
}
$names[$name] = 1;
$name .= '.eml';
if (!empty($loaded_attachments[$name . 'message/rfc822'])) {
continue;
}
if ($size_limit && $size_limit < $total_size + $message->headers->size) {
$size_errors++;
continue;
}
$total_size += $message->headers->size;
rcmail_save_attachment($message, null, $COMPOSE['id'], array('filename' => $name));
if ($message->headers->messageID) {
$refs[] = $message->headers->messageID;
}
}
// set In-Reply-To and References headers
if (count($refs) == 1) {
$COMPOSE['reply_msgid'] = $refs[0];
}
if (!empty($refs)) {
$COMPOSE['references'] = implode(' ', $refs);
}
if ($size_errors) {
$limit = $RCMAIL->show_bytes($size_limit);
$error = $RCMAIL->gettext(array('name' => 'msgsizeerrorfwd', 'vars' => array('num' => $size_errors, 'size' => $limit)));
$RCMAIL->output->add_script(sprintf("%s.display_message('%s', 'error');", rcmail_output::JS_OBJECT_NAME, rcube::JQ($error)), 'docready');
}
}
// Saves an image as attachment
function rcmail_save_image($path, $mimetype = '', $data = null)
{
global $COMPOSE;
// handle attachments in memory
if (empty($data)) {
$data = file_get_contents($path);
$is_file = true;
}
$name = rcmail_basename($path);
if (empty($mimetype)) {
if ($is_file) {
$mimetype = rcube_mime::file_content_type($path, $name);
}
else {
$mimetype = rcube_mime::file_content_type($data, $name, 'application/octet-stream', true);
}
}
$attachment = array(
'group' => $COMPOSE['id'],
'name' => $name,
'mimetype' => $mimetype,
'data' => $data,
'size' => strlen($data),
);
$attachment = rcmail::get_instance()->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status']) {
unset($attachment['data'], $attachment['status'], $attachment['content_id'], $attachment['abort']);
return $attachment;
}
return false;
}
// Unicode-safe basename()
function rcmail_basename($filename)
{
// basename() is not unicode safe and locale dependent
if (stristr(PHP_OS, 'win') || stristr(PHP_OS, 'netware')) {
return preg_replace('/^.*[\\\\\\/]/', '', $filename);
}
else {
return preg_replace('/^.*[\/]/', '', $filename);
}
}
/**
* Attachments list object for templates
*/
function rcmail_compose_attachment_list($attrib)
{
global $RCMAIL, $OUTPUT, $COMPOSE;
// add ID if not given
if (!$attrib['id'])
$attrib['id'] = 'rcmAttachmentList';
$out = "";
$jslist = array();
$button = '';
if ($attrib['icon_pos'] == 'left')
$COMPOSE['icon_pos'] = 'left';
if (is_array($COMPOSE['attachments'])) {
if ($attrib['deleteicon']) {
$button = html::img(array(
'src' => $RCMAIL->output->asset_url($attrib['deleteicon'], true),
'alt' => $RCMAIL->gettext('delete')
));
}
else if (rcube_utils::get_boolean($attrib['textbuttons'])) {
$button = rcube::Q($RCMAIL->gettext('delete'));
}
foreach ($COMPOSE['attachments'] as $id => $a_prop) {
if (empty($a_prop)) {
continue;
}
$link_content = sprintf('<span class="attachment-name" onmouseover="rcube_webmail.long_subject_title_ex(this)">%s</span> <span class="attachment-size">(%s)</span>',
rcube::Q($a_prop['name']), $RCMAIL->show_bytes($a_prop['size']));
$content_link = html::a(array(
'href' => "#load",
'class' => 'filename',
'onclick' => sprintf("return %s.command('load-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
+ 'tabindex' => $attrib['tabindex'] ?: '0',
), $link_content);
$delete_link = html::a(array(
'href' => "#delete",
'title' => $RCMAIL->gettext('delete'),
'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
'class' => 'delete',
'tabindex' => $attrib['tabindex'] ?: '0',
'aria-label' => $RCMAIL->gettext('delete') . ' ' . $a_prop['name'],
), $button);
$out .= html::tag('li', array(
- 'id' => 'rcmfile'.$id,
- 'class' => rcube_utils::file2class($a_prop['mimetype'], $a_prop['name']),
+ 'id' => 'rcmfile' . $id,
+ 'class' => rcube_utils::file2class($a_prop['mimetype'], $a_prop['name']),
),
$COMPOSE['icon_pos'] == 'left' ? $delete_link.$content_link : $content_link.$delete_link
);
$jslist['rcmfile'.$id] = array(
'name' => $a_prop['name'],
'complete' => true,
'mimetype' => $a_prop['mimetype']
);
}
}
if ($attrib['deleteicon'])
$COMPOSE['deleteicon'] = $RCMAIL->output->asset_url($attrib['deleteicon'], true);
else if (rcube_utils::get_boolean($attrib['textbuttons']))
$COMPOSE['textbuttons'] = true;
if ($attrib['cancelicon'])
$OUTPUT->set_env('cancelicon', $RCMAIL->output->asset_url($attrib['cancelicon'], true));
if ($attrib['loadingicon'])
$OUTPUT->set_env('loadingicon', $RCMAIL->output->asset_url($attrib['loadingicon'], true));
$OUTPUT->set_env('attachments', $jslist);
$OUTPUT->add_gui_object('attachmentlist', $attrib['id']);
// put tabindex value into data-tabindex attribute
if (isset($attrib['tabindex'])) {
$attrib['data-tabindex'] = $attrib['tabindex'];
unset($attrib['tabindex']);
}
return html::tag('ul', $attrib, $out, html::$common_attrib);
}
/**
* Attachment upload form object for templates
*/
function rcmail_compose_attachment_form($attrib)
{
global $RCMAIL;
return $RCMAIL->upload_form($attrib, 'uploadform', 'send-attachment', array('multiple' => true));
}
/**
* Register a certain container as active area to drop files onto
*/
function rcmail_compose_file_drop_area($attrib)
{
global $OUTPUT;
if ($attrib['id']) {
$OUTPUT->add_gui_object('filedrop', $attrib['id']);
$OUTPUT->set_env('filedrop', array('action' => 'upload', 'fieldname' => '_attachments'));
}
}
/**
* Editor mode selector object for templates
*/
function rcmail_editor_selector($attrib)
{
global $RCMAIL;
// determine whether HTML or plain text should be checked
$useHtml = rcmail_compose_editor_mode();
if (empty($attrib['editorid']))
$attrib['editorid'] = 'rcmComposeBody';
if (empty($attrib['name']))
$attrib['name'] = 'editorSelect';
$attrib['onchange'] = "return rcmail.command('toggle-editor', {id: '".$attrib['editorid']."', html: this.value == 'html'}, '', event)";
$select = new html_select($attrib);
$select->add(rcube::Q($RCMAIL->gettext('htmltoggle')), 'html');
$select->add(rcube::Q($RCMAIL->gettext('plaintoggle')), 'plain');
return $select->show($useHtml ? 'html' : 'plain');
}
/**
* Addressbooks list object for templates
*/
function rcmail_addressbook_list($attrib = array())
{
global $RCMAIL, $OUTPUT;
$attrib += array('id' => 'rcmdirectorylist');
$out = '';
$line_templ = html::tag('li', array(
'id' => 'rcmli%s', 'class' => '%s'),
html::a(array('href' => '#list',
'rel' => '%s',
'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('list-addresses','%s',this)"), '%s'));
foreach ($RCMAIL->get_address_sources(false, true) as $j => $source) {
$id = strval(strlen($source['id']) ? $source['id'] : $j);
$js_id = rcube::JQ($id);
// set class name(s)
$class_name = 'addressbook';
if ($source['class_name'])
$class_name .= ' ' . $source['class_name'];
$out .= sprintf($line_templ,
rcube_utils::html_identifier($id,true),
$class_name,
$source['id'],
$js_id, ($source['name'] ?: $id));
}
$OUTPUT->add_gui_object('addressbookslist', $attrib['id']);
return html::tag('ul', $attrib, $out, html::$common_attrib);
}
/**
* Contacts list object for templates
*/
function rcmail_contacts_list($attrib = array())
{
global $RCMAIL, $OUTPUT;
$attrib += array('id' => 'rcmAddressList');
// set client env
$OUTPUT->add_gui_object('contactslist', $attrib['id']);
$OUTPUT->set_env('pagecount', 0);
$OUTPUT->set_env('current_page', 0);
$OUTPUT->include_script('list.js');
return $RCMAIL->table_output($attrib, array(), array('name'), 'ID');
}
/**
* Responses list object for templates
*/
function rcmail_compose_responses_list($attrib)
{
global $RCMAIL, $OUTPUT;
$attrib += array('id' => 'rcmresponseslist', 'tagname' => 'ul', 'cols' => 1);
$jsenv = array();
$list = new html_table($attrib);
foreach ($RCMAIL->get_compose_responses(true) as $response) {
$key = $response['key'];
$item = html::a(array(
'href' => '#' . urlencode($response['name']),
'class' => rtrim('insertresponse ' . $attrib['itemclass']),
'unselectable' => 'on',
'tabindex' => '0',
'rel' => $key,
), rcube::Q($response['name']));
$jsenv[$key] = $response;
$list->add(array(), $item);
}
// set client env
$OUTPUT->set_env('textresponses', $jsenv);
$OUTPUT->add_gui_object('responseslist', $attrib['id']);
return $list->show();
}
diff --git a/skins/elastic/styles/widgets/lists.less b/skins/elastic/styles/widgets/lists.less
index 119e72954..e0051839a 100644
--- a/skins/elastic/styles/widgets/lists.less
+++ b/skins/elastic/styles/widgets/lists.less
@@ -1,1007 +1,1009 @@
/**
* Roundcube Webmail styles for the Elastic skin
*
* Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original authors in the README.md file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*/
/*** List and treelist widgets ***/
.listing {
tbody td,
li {
border-bottom: 1px solid @color-list-border;
cursor: default;
font-weight: normal;
line-height: @listing-line-height;
}
tbody td,
li a {
padding: 0 .5rem;
white-space: nowrap;
vertical-align: middle;
color: @color-list;
}
tbody td {
.overflow-ellipsis;
outline: none;
a {
color: @color-list;
}
}
li a {
display: block;
text-decoration: none;
cursor: default;
width: 100%;
}
li.selected,
tr.selected td {
color: @color-list-selected;
background-color: @color-list-selected-background;
}
td.selection {
padding: 0 0 0 .5em;
width: 2em;
text-align: center;
& > input {
vertical-align: middle;
}
}
&:not(.withselection) td.selection {
display: none;
}
td.name {
.overflow-ellipsis;
}
td.action {
padding: 0 .5em;
width: 2em;
text-align: center;
&:empty {
width: 0;
}
a {
display: block;
overflow: hidden;
text-decoration: none;
&:before {
&:extend(.font-icon-class);
margin: 0;
font-size: 1rem;
}
}
a.pushgroup:before {
content: @fa-var-chevron-right;
}
}
li.droptarget > a,
tr.droptarget > td {
background-color: @color-list-droptarget-background;
}
li.disabled,
tr.disabled td {
color: @color-list-deleted;
}
li > a.virtual,
li.virtual > a {
opacity: .4;
}
span.secondary {
color: @color-list-secondary;
}
}
// Focus indicator
html:not(.touch) {
.listing {
li > a,
tbody tr > td:first-child,
&:not(.withselection) tbody tr > td.selection + td {
border-left: 2px solid transparent;
}
li > a:focus,
&.focus tbody tr.focused > td:first-child,
&.focus:not(.withselection) tbody tr.focused > td.selection + td {
border-left: 2px solid @color-list-focus-indicator;
outline: 0;
}
}
}
table.listing {
width: 100%;
table-layout: fixed;
// border-spacing/border-collapse here fix problem with our focus indicator
// when the table cells use overflow: hidden. I.e. we use border-spacing:0
// instead of Bootstrap's border-collapse:collapse. Is this cross-browser?
border-spacing: 0;
border-collapse: unset;
}
ul.listing {
margin: 0;
padding: 0;
& > ul {
padding: 0;
}
li {
.overflow-ellipsis;
white-space: nowrap;
position: relative;
list-style: none;
ul {
border-top: 1px solid @color-list-border;
padding-left: 1.5em;
li:last-child {
border-bottom: none;
}
}
.custom-switch {
position: absolute;
padding: 0;
top: 0;
right: 0;
height: @listing-line-height;
vertical-align: middle;
.custom-control-label {
&:before,
&:after {
margin-top: .4rem;
html.touch & {
margin-top: .75rem;
}
}
}
}
}
&.simplelist {
li {
padding: 0 .5rem;
}
}
}
.listing-info {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
text-align: center;
font-weight: bold;
color: @color-list-secondary;
}
html.touch {
.listing:not(.toolbar) li,
.listing tbody td {
line-height: @listing-touch-line-height;
font-size: 1.2rem;
}
li input[type=checkbox] {
height: @listing-touch-line-height;
}
td.selection {
padding: 0;
width: 3em;
}
}
@media screen and (max-width: @screen-width-large) {
.listing.selection-large-only {
li.selected {
color: @color-list;
background-color: transparent;
}
}
}
/* icons */
.listing.iconized li {
a:before {
&:extend(.font-icon-class);
height: 2em; // TODO: ?
margin-right: .5rem;
}
&.preferences > a:before {
content: @fa-var-sliders-h;
}
&.folders > a:before {
content: @fa-var-folder;
}
&.responses > a:before {
content: @fa-var-comment;
}
&.identities > a:before {
content: @fa-var-id-card;
}
&.password > a:before {
content: @fa-var-lock;
}
&.addressbook a:before {
.font-icon-regular(@fa-var-address-book);
}
&.contactgroup a:before {
.font-icon-solid(@fa-var-users);
}
&.contactsearch a:before {
content: @fa-var-search;
}
&.filter > a:before {
content: @fa-var-filter;
}
&.vacation > a:before {
.font-icon-regular(@fa-var-clock);
}
&.forward > a:before {
content: @fa-var-share-square;
}
&.enigma.keys > a:before {
content: @fa-var-key;
}
&.userinfo > a:before {
content: @fa-var-info-circle;
}
&.twofactorauth > a:before {
content: @fa-var-sign-in-alt;
}
a.help:before {
content: @fa-var-life-ring;
}
a.about:before {
.font-icon-regular(@fa-var-question-circle);
}
a.license:before {
content: @fa-var-shield-alt;
}
// autocomplete popup
& > i:before {
&:extend(.font-icon-class);
content: @fa-var-user;
margin-left: .5rem;
}
&.group > i:before {
content: @fa-var-users;
}
}
html.ie11 .listing.iconized li a:before {
font-size: 1.25rem;
}
.listing.iconized tr {
td:before {
&:extend(.font-icon-class);
margin-right: .5rem;
}
&.contact.person td.name:before {
content: @fa-var-user;
}
&.contact.group td.name:before {
content: @fa-var-users;
}
&.general > td.section:before {
content: @fa-var-desktop;
}
&.mailbox > td.section:before {
.font-icon-regular(@fa-var-envelope);
}
&.mailview > td.section:before {
content: @fa-var-inbox;
}
&.compose > td.section:before {
content: @fa-var-paper-plane;
}
&.addressbook > td.section:before {
content: @fa-var-users;
}
&.folders > td.section:before {
.font-icon-regular(@fa-var-folder);
}
&.server > td.section:before {
content: @fa-var-server;
}
&.enigma > td.section:before {
content: @fa-var-lock;
}
&.calendar > td.section:before {
content: @fa-var-calendar;
}
&.chat > td.section:before {
content: @fa-var-comments;
}
}
/* selecatable list: e.g. spellcheck language selection */
.listing.iconized.selectable li {
a:before {
&:extend(.font-icon-class);
content: "";
}
a.selected:before {
content: @fa-var-check;
}
}
.popupmenu .listing {
li > a {
border-left: 0;
&:not(.disabled):hover {
color: @color-menu-hover;
background-color: @color-menu-hover-background;
}
}
li.selected {
color: @color-menu-hover;
background-color: @color-menu-hover-background;
}
td {
.overflow-ellipsis;
}
}
ul.treelist {
li {
div.treetoggle {
position: absolute;
top: 0;
left: 0;
width: @listing-treetoggle-width;
cursor: pointer;
background-color: transparent;
&:before {
&:extend(.font-icon-class);
content: @fa-var-angle-right;
margin-left: .25em;
font-size: 1em;
}
&.expanded:before {
content: @fa-var-angle-down;
}
}
& > a {
.overflow-ellipsis;
padding-left: @listing-treetoggle-width;
}
&.selected {
// reset .listing selection style
color: inherit;
background-color: transparent;
& > div > a, // this is used e.g. by kolab_addressbook
& > a {
color: @color-list-selected;
background-color: @color-list-selected-background;
}
}
ul {
padding: 0;
li {
padding-left: 0;
a { padding-left: (2 * @listing-treetoggle-width); }
div.treetoggle { left: @listing-treetoggle-width; }
li {
a { padding-left: (3 * @listing-treetoggle-width); }
div.treetoggle { left: (2 * @listing-treetoggle-width); }
li {
a { padding-left: (4 * @listing-treetoggle-width); }
div.treetoggle { left: (3 * @listing-treetoggle-width); }
li {
a { padding-left: (5 * @listing-treetoggle-width); }
div.treetoggle { left: (4 * @listing-treetoggle-width); }
li {
a { padding-left: (6 * @listing-treetoggle-width); }
div.treetoggle { left: (5 * @listing-treetoggle-width); }
}
}
}
}
}
}
}
&.notree {
div.treetoggle {
display: none;
}
li > a {
padding-left: .5em;
}
}
}
/*** Folders list widget ***/
.folderlist {
li {
&.mailbox {
&.unread {
& > a {
padding-right: 2.8em;
font-weight: bold;
}
}
&.recent {
color: @color-list-recent;
}
.unreadcount {
position: absolute;
top: 0;
right: 0;
min-width: 2em;
line-height: 1.4rem;
margin: (@listing-line-height - 1.4 * @page-font-size) / 2;
padding: 0 .3em;
border-radius: .4em;
background: @color-list-badge-background;
color: @color-list-badge;
text-align: center;
font-weight: bold;
html.touch & {
line-height: 2rem;
margin: (@listing-touch-line-height - 2 * @page-font-size) / 2;
}
}
&.recent > .unreadcount {
background: @color-list-recent-badge-background;
color: @color-list-recent-badge;
}
&.root {
display: none !important;
// FIXME: This element is confusing, I propose to not use it
}
}
a:before {
&:extend(.font-icon-class);
.font-icon-regular(@fa-var-folder);
margin-right: .5rem;
}
&.inbox > a:before {
.font-icon-solid(@fa-var-inbox);
}
&.trash a:before {
.font-icon-solid(@fa-var-trash-alt);
}
&.trash.empty > a:before {
.font-icon-regular(@fa-var-trash-alt);
}
&.drafts a:before {
.font-icon-solid(@fa-var-pencil-alt);
}
&.sent a:before {
.font-icon-solid(@fa-var-paper-plane);
}
&.junk a:before {
.font-icon-solid(@fa-var-fire-alt);
}
&.archive > a:before {
.font-icon-solid(@fa-var-archive);
}
}
// folder-selector fix for left padding
&.menu a:before {
margin-left: .5em;
}
}
/*** Messages list widget ***/
.messagelist > thead,
.messagelist .branch,
table.fixedcopy {
display: none;
}
.messagelist {
td {
border-left: 0;
width: 2em;
vertical-align: top;
font-size: 1rem !important;
}
td.subject {
width: 100%;
padding-right: 0;
display: flex;
flex-wrap: wrap;
a {
text-decoration: none;
cursor: default;
}
span {
line-height: 2em;
&.date {
font-size: 90%;
color: @color-list-secondary;
}
&.fromto {
.overflow-ellipsis;
flex: 1;
font-size: 90%;
color: @color-list-secondary;
padding-left: 1.5em;
padding-right: .5rem;
}
&.subject {
.overflow-ellipsis;
width: 100%;
}
}
}
td.threads {
padding: 0 0 0 .25rem;
width: 1.5em;
}
td.flags {
width: 2.5em;
& > span {
height: 1.7em;
line-height: 1.7em;
display: block;
&.flag {
cursor: pointer;
}
}
}
tr.flagged td,
tr.flagged td.subject span.subject a,
tr.flagged td.subject span.date,
tr.flagged td.subject span.fromto {
color: @color-list-flagged;
}
tr.deleted td,
tr.deleted td.subject span.subject a,
tr.deleted td.subject span.date,
tr.deleted td.subject span.fromto {
color: @color-list-deleted;
}
tr.unread td.subject span.subject {
font-weight: bold;
}
// thread parent message with unread children
tr.unroot td.subject a {
text-decoration: underline;
}
tr.thread td.threads div:before {
&:extend(.font-icon-class);
content: @fa-var-angle-right;
cursor: pointer;
width: 1em;
}
tr.thread.expanded td.threads div:before {
content: @fa-var-angle-down;
}
td.subject span.msgicon.status {
&:before {
&:extend(.font-icon-class);
content: @fa-var-circle;
cursor: pointer;
font-size: .4rem;
width: 1.1rem;
height: 2rem;
}
&.unread:before {
content: @fa-var-circle;
color: @color-list-unread-status;
font-size: .5rem;
}
&.unreadchildren:before {
.font-icon-regular(@fa-var-circle);
font-size: .5rem;
}
&.replied:before {
.font-icon-solid(@fa-var-reply);
font-size: 1rem;
}
&.forwarded:before {
.font-icon-solid(@fa-var-share);
font-size: 1rem;
}
&.replied.forwarded:before {
.font-icon-solid(@fa-var-reply); // TODO
font-size: 1rem;
}
tr.deleted &:before {
.font-icon-solid(@fa-var-ban);
font-size: 1rem;
}
}
span.attachment span {
&:extend(.font-icon-class);
color: @color-list-icon;
&:before {
margin: 0;
content: @fa-var-paperclip;
}
&.report:before {
.font-icon-regular(@fa-var-file-alt);
}
&.encrypted:before {
content: @fa-var-lock;
}
&.vcard:before {
.font-icon-regular(@fa-var-user); // vcard_attachments plugin
}
}
span.flagged:before {
&:extend(.font-icon-class);
content: @fa-var-flag;
}
tr:hover span.unflagged:before {
&:extend(.font-icon-class);
.font-icon-regular(@fa-var-flag);
}
}
// On touch devices hide flag icon, but do it in a way
// that saves as much room as possible, keeping the attachment icon
html.layout-phone,
html.touch {
.messagelist {
tr {
position: relative;
}
td.flags {
top: .25rem;
right: 0;
bottom: 0;
.flag {
visibility: hidden;
}
}
td.subject {
padding-right: .5em;
.subject {
padding-right: 1.5rem;
}
}
}
}
/* Contacts list */
.contactlist {
.contact.readonly td {
font-style: italic;
}
td.action {
// TODO
a {
// TODO
}
}
// for contacts list in mail compose
td.contact:before {
&:extend(.font-icon-class);
content: @fa-var-user;
}
// for contacts list in mail compose
td.contactgroup:before {
&:extend(.font-icon-class);
content: @fa-var-users;
}
span.email {
display: inline;
color: @color-list-secondary;
font-style: italic;
margin-left: .5em;
}
li {
a:before {
&:extend(.font-icon-class);
margin-right: .5rem;
}
a.addressbook::before {
.font-icon-regular(@fa-var-address-book);
}
a.contactgroup::before {
.font-icon-solid(@fa-var-users);
}
}
}
/* Attachments list */
@attachmentslist-item-height: 2rem;
.attachmentslist {
padding: 0;
margin: 0;
background-color: @color-attachmentlist-background;
border: 1px solid @color-attachmentlist-border;
&:empty {
padding: 0;
border: 0;
}
li {
list-style: none;
display: inline-flex;
white-space: nowrap;
line-height: @attachmentslist-item-height;
padding: 0 .25em;
max-width: 100%;
&:before {
&:extend(.font-icon-class);
.font-icon-regular(@fa-var-file);
height: @attachmentslist-item-height;
+ margin: 0;
}
&.txt:before,
&.text:before {
.font-icon-regular(@fa-var-file-alt);
}
&.pdf:before {
.font-icon-regular(@fa-var-file-pdf);
}
&.odt:before,
&.doc:before,
&.docx:before,
&.msword:before {
.font-icon-regular(@fa-var-file-word);
}
&.ods:before,
&.xls:before,
&.xlsx:before,
&.msexcel:before {
.font-icon-regular(@fa-var-file-excel);
}
&.rar:before,
&.zip:before,
&.gz:before {
.font-icon-regular(@fa-var-file-archive);
}
&.image:before,
&.jpg:before,
&.jpeg:before,
&.png:before {
.font-icon-regular(@fa-var-file-image);
}
&.mp3:before,
&.audio:before {
.font-icon-regular(@fa-var-file-audio);
}
&.m4p:before,
&.video:before {
.font-icon-regular(@fa-var-file-video);
}
&.ics:before,
&.calendar:before {
// TODO
}
&.vcard:before {
.font-icon-regular(@fa-var-address-card);
}
&.html:before {
.font-icon-regular(@fa-var-file-code);
}
&.eml:before,
&.rfc822:before {
// TODO
}
&.odp:before,
&.otp:before,
&.ppt:before,
&.pptx:before,
&.ppsx:before,
&.vnd.mspowerpoint:before {
.font-icon-regular(@fa-var-file-powerpoint);
}
&.sig:before,
&.pgp-signature:before,
&.pkcs7-signature:before {
// TODO
}
&.application.asc:before {
// TODO
}
&.application.pgp-keys:before {
// TODO
}
a {
text-decoration: none;
line-height: @attachmentslist-item-height;
height: @attachmentslist-item-height;
}
a.cancelupload:before,
a.delete:before {
&:extend(.font-icon-class);
content: @fa-var-trash-alt;
line-height: @attachmentslist-item-height;
height: @attachmentslist-item-height;
margin: 0;
}
a.dropdown:before {
margin: 0;
}
&.uploading:before {
.animated-icon-class;
.font-icon-solid(@fa-var-circle-notch);
}
a.filename {
display: flex;
overflow: hidden;
+ padding: 0 .2em;
}
.attachment-name {
.overflow-ellipsis;
color: @color-font;
}
.attachment-size {
color: @color-list-secondary;
- padding: 0 .25em;
+ padding-left: .25em;
}
}
}
.keylist {
padding: 0;
list-style: none;
li {
line-height: 2;
&:before {
&:extend(.font-icon-class);
content: @fa-var-key;
line-height: 1.5;
}
}
}
#identities-table {
td.mail:before {
&:extend(.font-icon-class);
content: @fa-var-id-card;
}
}
#responses-table {
td.name:before {
&:extend(.font-icon-class);
content: @fa-var-comment;
}
}
#filterslist {
td.name:before {
&:extend(.font-icon-class);
content: @fa-var-filter;
}
}
#filtersetslist {
td.name:before {
&:extend(.font-icon-class);
content: @fa-var-file-alt;
}
}
#subscription-table {
li.mailbox a {
padding-right: 2.5rem;
}
}
diff --git a/skins/elastic/ui.js b/skins/elastic/ui.js
index f6f51b12c..50db96f2c 100644
--- a/skins/elastic/ui.js
+++ b/skins/elastic/ui.js
@@ -1,3965 +1,3972 @@
/**
* Roundcube webmail functions for the Elastic skin
*
* Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*
* @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
*/
"use strict";
function rcube_elastic_ui()
{
var ref = this,
mode = 'normal', // one of: large, normal, small, phone
touch = false,
ios = false,
popups_close_lock,
is_framed = rcmail.is_framed(),
env = {
config: {
standard_windows: rcmail.env.standard_windows,
message_extwin: rcmail.env.message_extwin,
compose_extwin: rcmail.env.compose_extwin,
help_open_extwin: rcmail.env.help_open_extwin
},
checkboxes: 0,
small_screen_config: {
standard_windows: true,
message_extwin: false,
compose_extwin: false,
help_open_extwin: false
}
},
menus = {},
content_buttons = [],
frame_buttons = [],
layout = {
menu: $('#layout-menu'),
sidebar: $('#layout-sidebar'),
list: $('#layout-list'),
content: $('#layout-content'),
},
buttons = {
menu: $('a.task-menu-button'),
back_sidebar: $('a.back-sidebar-button'),
back_list: $('a.back-list-button'),
back_content: $('a.back-content-button'),
};
// Public methods
this.register_content_buttons = register_content_buttons;
this.menu_hide = menu_hide;
this.menu_toggle = menu_toggle;
this.menu_destroy = menu_destroy;
this.popup_init = popup_init;
this.about_dialog = about_dialog;
this.headers_dialog = headers_dialog;
this.import_dialog = import_dialog;
this.headers_show = headers_show;
this.spellmenu = spellmenu;
this.searchmenu = searchmenu;
this.headersmenu = headersmenu;
this.header_reset = header_reset;
this.compose_status = compose_status;
this.attachmentmenu = attachmentmenu;
this.mailtomenu = mailtomenu;
this.recipient_selector = recipient_selector;
this.show_list = show_list;
this.show_sidebar = show_sidebar;
this.smart_field_init = smart_field_init;
this.smart_field_reset = smart_field_reset;
this.form_errors = form_errors;
this.switch_nav_list = switch_nav_list;
this.searchbar_init = searchbar_init;
this.pretty_checkbox = pretty_checkbox;
this.pretty_select = pretty_select;
this.datepicker_init = datepicker_init;
this.bootstrap_style = bootstrap_style;
// Detect screen size/mode
screen_mode();
// Initialize layout
layout_init();
// Convert some elements to Bootstrap style
bootstrap_style();
// Initialize responsive toolbars (have to be before popups init)
toolbar_init();
// Initialize content frame and list handlers
content_frame_init();
// Initialize menu dropdowns
dropdowns_init();
// Setup various UI elements
setup();
// Update layout after initialization
resize();
/**
* Setup procedure
*/
function setup()
{
var title, form, content_buttons = [];
// Intercept jQuery-UI dialogs...
$.ui && $.widget('ui.dialog', $.ui.dialog, {
open: function() {
// .. to unify min width for iframe'd dialogs
if ($(this.element).is('.iframe')) {
this.options.width = Math.max(576, this.options.width);
}
this._super();
// ... to re-style them on dialog open
dialog_open(this);
return this;
},
close: function() {
this._super();
// ... to close custom select dropdowns on dialog close
$('.select-menu:visible').remove();
return this;
}
});
// menu/sidebar/list button
buttons.menu.on('click', function() { app_menu(true); return false; });
buttons.back_sidebar.on('click', function() { show_sidebar(); return false; });
buttons.back_list.on('click', function() { show_list(); return false; });
buttons.back_content.on('click', function() { show_content(true); return false; });
// Initialize search forms
$('.searchbar').each(function() { searchbar_init(this); });
// Set content frame title in parent window (exclude ext-windows and dialog frames)
if (is_framed && !rcmail.env.extwin && !parent.$('.ui-dialog:visible').length) {
if (title = $('h1.voice').first().text()) {
parent.$('#layout-content > .header > .header-title:not(.constant)').text(title);
}
}
else if (!is_framed) {
title = $('.boxtitle', layout.content).first().detach().text();
if (!title) {
title = $('h1.voice').first().text();
}
if (title) {
$('.header > .header-title', layout.content).text(title);
}
}
// Add content frame toolbar in the footer, for content buttons and navigation
if (!is_framed && layout.content.length && !$(layout.content).is('.no-navbar')
&& !$(layout.content).children('.frame-content').length
) {
env.frame_nav = $('<div class="footer menu toolbar content-frame-navigation hide-nav-buttons">')
.append($('<a class="button prev">')
.append($('<span class="inner"></span>').text(rcmail.gettext('previous'))))
.append($('<span class="buttons">'))
.append($('<a class="button next">')
.append($('<span class="inner"></span>').text(rcmail.gettext('next'))))
.appendTo(layout.content);
}
// Move some buttons to the frame footer toolbar
$('a[data-content-button]').each(function() {
content_buttons.push(create_cloned_button($(this)));
});
// Move form buttons from the content frame into the frame footer (on parent window)
$('.formbuttons').filter(function() { return !$(this).parent('.searchoptions').length; }).children().each(function() {
var target = $(this);
// skip non-content buttons
if (!is_framed && !target.parents('#layout-content').length) {
return;
}
if (target.is('.cancel')) {
target.addClass('hidden');
return;
}
content_buttons.push(create_cloned_button(target));
});
(is_framed ? parent.UI : ref).register_content_buttons(content_buttons);
// Mail compose features
if (form = rcmail.gui_objects.messageform) {
form = $('form[name="' + form + '"]');
// Show input elements with non-empty value
// These event handlers need to be registered before rcmail 'init' event
$('#_cc, #_bcc, #_replyto, #_followupto', $('.compose-headers')).each(function() {
$(this).on('change', function() {
$('#compose' + $(this).attr('id'))[this.value ? 'removeClass' : 'addClass']('hidden');
});
});
// We put compose options outside of the main form
// Because IE/Edge (<16) does not support 'form' attribute we'll copy
// inputs into the main form as hidden fields
// TODO: Consider doing this for IE/Edge only, just set the 'form' attribute on others
$('#compose-options').find('textarea,input,select').each(function() {
var hidden = $('<input>')
.attr({type: 'hidden', name: $(this).attr('name')})
.appendTo(form);
$(this).attr('tabindex', 2)
.on('change', function() {
hidden.val(this.type != 'checkbox' || this.checked ? $(this).val() : '');
})
.change();
});
}
// Use smart recipient inputs
// This have to be after mail compose feature above
$('[data-recipient-input]').each(function() { recipient_input(this); });
// Image upload widget
$('.image-upload').each(function() { image_upload_input(this); });
// Add HTML/Plain tabs (switch) on top of textarea with TinyMCE editor
$('textarea[data-html-editor]').each(function() { html_editor_init(this); });
$('#dragmessage-menu,#dragcontact-menu').each(function() {
rcmail.gui_object('dragmenu', this.id);
});
// Taskmenu items added by plugins do not use elastic classes (e.g help plugin)
// it's for larry skin compat. We'll assign 'selected' and icon-specific class.
$('#taskmenu > a').each(function() {
if (/button-([a-z]+)/.test(this.className)) {
var data, name = RegExp.$1,
button = find_button(this.id);
if (button && (data = button.data)) {
if (data.sel) {
data.sel = data.sel.replace('button-selected', 'selected') + ' ' + name;
}
if (data.act) {
data.act += ' ' + name;
}
rcmail.buttons[button.command][button.index] = data;
rcmail.init_button(button.command, data);
}
$(this).addClass(name);
$('.button-inner', this).addClass('inner');
}
$(this).on('mouseover', function() { rcube_webmail.long_subject_title(this, 0, $('span.inner', this)); });
});
// Some plugins use 'listbubtton' class, we'll replace it with 'button'
$('.listbutton').each(function() {
var button = find_button(this.id);
$(this).addClass('button').removeClass('listbutton');
if (button.data.sel) {
button.data.sel = button.data.sel.replace('listbutton', 'button');
}
if (button.data.act) {
button.data.act = button.data.act.replace('listbutton', 'button');
}
rcmail.buttons[button.command][button.index] = button.data;
rcmail.init_button(button.command, button.data);
});
// buttons that should be hidden on small screen devices
$('[data-hidden]').each(function() {
var m, v = $(this).data('hidden'),
parent = $(this).parent('li'),
re = /(large|big|small|phone|lbs)/g;
while (m = re.exec(v)) {
$(parent.length ? parent : this).addClass('hidden-' + m[1]);
}
});
// Modify normal checkboxes on lists so they are different
// than those used for row selection, i.e. use icons
$('[data-list]').each(function() {
$('input[type=checkbox]', this).each(function() { pretty_checkbox(this); });
});
// Assign .formcontainer class to the iframe body, when it
// contains .formcontent and .formbuttons.
if (is_framed) {
$('.formcontent').each(function() {
if ($(this).next('.formbuttons').length) {
$(this).parent().addClass('formcontainer');
}
});
}
// move "Download all attachments" button into a better location
$('#attachment-list + a.zipdownload').appendTo('.header-links');
if (ios = $('html').is('.ipad,.iphone')) {
$('.iframe-wrapper, .scroller').addClass('ios-scroll');
}
if ($('html').filter('.ipad,.iphone,.webkit.mobile,.webkit.tablet').addClass('webkit-scroller').length) {
$(layout.menu).addClass('webkit-scroller');
}
// Set .notree class on treelist widget update
$('.treelist').each(function() {
var list = this, callback = function() {
$(list)[$('.treetoggle', list).length > 0 ? 'removeClass' : 'addClass']('notree');
};
if (window.MutationObserver) {
(new MutationObserver(callback)).observe(list, {childList: true, subtree: true});
}
callback();
// Add title with full folder name on hover
// TODO: This should be done in another way, so if an entry is
// added after page load it also works there.
$('li.mailbox > a').on('mouseover', function() { rcube_webmail.long_subject_title_ex(this); });
});
// Store default logo path if not already set
if (!$('#logo').data('src-default')) {
$('#logo').data('src-default', $('#logo').attr('src'));
}
};
/**
* Moves form buttons into the content frame actions toolbar (for mobile)
*/
function register_content_buttons(buttons)
{
// we need these buttons really only in phone mode
if (/*mode == 'phone' && */ env.frame_nav && buttons && buttons.length) {
var toolbar = env.frame_nav.children('.buttons');
content_buttons = [];
$.each(buttons, function() {
if (this.data('target')) {
content_buttons.push(this.data('target'));
}
});
toolbar.html('').append(buttons);
}
};
/**
* Registers cloned button
*/
function register_cloned_button(old_id, new_id, active_class)
{
var button = find_button(old_id);
if (button) {
rcmail.register_button(button.command, new_id, button.data.type, active_class, button.data.sel);
}
};
/**
* Create a button clone for use in toolbar
*/
function create_cloned_button(target, menu_button, add_class, always_active)
{
var popup, click = true,
button = $('<a>'),
target_id = target.attr('id') || new Date().getTime(),
button_id = target_id + '-clone',
btn_class = target[0].className + (add_class ? ' ' + add_class : '');
if (!menu_button) {
btn_class = $.trim(btn_class.replace('btn-primary', 'primary').replace(/(btn[a-z-]*|button|disabled)/g, ''))
btn_class += ' button' + (!always_active ? ' disabled' : '');
}
else if (popup = target.data('popup')) {
button.data({popup: popup, 'toggle-button': target.data('toggle-button')});
popup_init(button[0]);
click = false;
rcmail.register_menu_button(button[0], popup);
}
button.attr({id: button_id, href: '#', 'class': btn_class})
.append($('<span class="inner">').text(target.text()));
if (click) {
button.on('click', function(e) { target.click(); });
}
if (is_framed && !menu_button) {
button.data('target', target);
frame_buttons.push($.extend({button_id: button_id}, find_button(target[0].id)));
}
else {
// Register the button to get active state updates
register_cloned_button(target_id, button_id, btn_class.replace(' disabled', ''));
}
return button;
};
/**
* Finds an rcmail button
*/
function find_button(id)
{
var i, button, command;
for (command in rcmail.buttons) {
for (i = 0; i < rcmail.buttons[command].length; i++) {
button = rcmail.buttons[command][i];
if (button.id == id) {
return {
command: command,
index: i,
data: button
};
}
}
}
};
/**
* Setup environment
*/
function layout_init()
{
// Select current layout element
env.last_selected = $('#layout > div.selected')[0];
if (!env.last_selected && layout.content.length) {
$.each(['sidebar', 'list', 'content'], function() {
if (layout[this].length) {
env.last_selected = layout[this][0];
layout[this].addClass('selected');
return false;
}
});
}
// Register resize handler
$(window).on('resize', function() {
clearTimeout(env.resize_timeout);
env.resize_timeout = setTimeout(function() { resize(); }, 25);
});
// Enable rcmail.open_window intercepting
env.open_window = rcmail.open_window;
rcmail.open_window = window_open;
rcmail
.addEventListener('message', message_displayed)
.addEventListener('menu-open', menu_toggle)
.addEventListener('menu-close', menu_toggle)
.addEventListener('editor-init', tinymce_init)
.addEventListener('autocomplete_create', rcmail_popup_init)
.addEventListener('googiespell_create', rcmail_popup_init)
.addEventListener('setquota', update_quota)
.addEventListener('enable-command', enable_command_handler)
.addEventListener('init', init);
// Add styling for TinyMCE editor popups
// We need to use MutationObserver, as TinyMCE does not provide any events for this
if (window.MutationObserver && window.tinymce) {
var callback = function(list) {
$.each(list, function() {
$.each(this.addedNodes, function() {
tinymce_style(this);
});
});
};
(new MutationObserver(callback)).observe(document.body, {childList: true});
}
};
/**
* rcmail 'init' event handler
*/
function init()
{
// Additional functionality on list widgets
$('[data-list]').filter('ul,table').each(function() {
var button,
table = $(this),
list = table.data('list');
if (rcmail[list] && rcmail[list].multiselect) {
var repl, button,
parent = table.parents('layout-sidebar,#layout-list,#layout-content').last(),
header = parent.find('.header'),
toolbar = header.find('ul');
if (!toolbar.length) {
toolbar = header;
}
else if (button = toolbar.find('a.select').data('toggle-button')) {
button = $('#' + button);
}
// Enable checkbox selection on list widgets
rcmail[list].enable_checkbox_selection();
// Add Select button to the list navigation bar
if (!button) {
button = $('<a>').attr({'class': 'button select disabled', role: 'button', title: rcmail.gettext('select')})
.on('click', function() { if ($(this).is('.active')) table.toggleClass('withselection'); })
.append($('<span class="inner">').text(rcmail.gettext('select')));
if (toolbar.is('.menu')) {
button.prependTo(toolbar).wrap('<li role="menuitem">');
// Add a button to the content toolbar menu too
if (layout.content) {
var button2 = create_cloned_button(button, true, 'hidden-big hidden-large');
$('<li role="menuitem">').append(button2).appendTo('#toolbar-menu');
button = button.add(button2);
}
}
else {
if (repl = table.data('list-select-replace')) {
$(repl).replaceWith(button);
}
else {
button.appendTo(toolbar).addClass('icon');
if (!parent.is('#layout-sidebar')) {
button.addClass('toolbar-button');
}
}
}
}
// Update Select button state on list update
rcmail.addEventListener('listupdate', function(prop) {
if (prop.list && prop.list == rcmail[list]) {
if (prop.rowcount) {
button.addClass('active').removeClass('disabled').attr('tabindex', 0);
}
else {
button.removeClass('active').addClass('disabled').attr('tabindex', -1);
}
}
});
}
// https://github.com/roundcube/elastic/issues/45
// Draggable blocks scrolling on touch devices, we'll disable it there
if (touch && rcmail[list]) {
if (typeof rcmail[list].draggable == 'function') {
rcmail[list].draggable('destroy');
}
else if (typeof rcmail[list].draggable == 'boolean') {
rcmail[list].draggable = false;
}
}
});
// Display "List is empty..." on the list
if (window.MutationObserver) {
$('[data-label-msg]').filter('ul,table').each(function() {
var fn, observer, callback,
info = $('<div class="listing-info hidden">').insertAfter(this),
table = $(this),
fn = function() {
var ext, command,
msg = table.data('label-msg'),
list = table.is('ul') ? table : table.children('tbody');
if (!rcmail.env.search_request && !rcmail.env.qsearch
&& msg && !list.children(':visible').length
) {
ext = table.data('label-ext');
command = table.data('create-command');
if (ext && (!command || rcmail.commands[command])) {
msg += ' ' + ext;
}
info.text(msg).removeClass('hidden');
return;
}
info.addClass('hidden');
};
callback = function() {
// wait until the UI stops loading and the list is visible
if (rcmail.busy || !table.is(':visible')) {
return setTimeout(callback, 250);
}
clearTimeout(env.list_timer);
env.list_timer = setTimeout(fn, 50);
};
// show/hide the message when something changes on the list
observer = new MutationObserver(callback);
observer.observe(table[0], {childList: true, subtree: true, attributes: true, attributeFilter: ['style']});
// initialize the message
callback();
});
}
// Create floating action button(s)
if ((layout.list.length || layout.content.length) && is_mobile()) {
var fabuttons = [];
$('[data-fab]').each(function() {
var button = $(this),
task = button.data('fab-task') || '*',
action = button.data('fab-action') || '*';
if ((task == '*' || task == rcmail.task)
&& (action == '*' || action == rcmail.env.action || (action == 'none' && !rcmail.env.action))
) {
fabuttons.push(create_cloned_button(button, false, false, true));
}
});
if (fabuttons.length) {
$('<div class="floating-action-buttons">').append(fabuttons)
.appendTo(layout.list.length ? layout.list : layout.content);
}
}
// Add menu link for each attachment
if (rcmail.env.action != 'print') {
$('#attachment-list > li').each(function() {
attachmentmenu_append(this);
});
}
var phone_confirmation = function(label) {
if (mode == 'phone') {
rcmail.display_message(rcmail.gettext(label), 'confirmation');
}
};
rcmail.addEventListener('fileappended', function(e) {
if (e.attachment.complete) {
attachmentmenu_append(e.item);
if (e.attachment.mimetype == 'text/vcard' && rcmail.commands['attach-vcard']) {
phone_confirmation('vcard_attachments.vcardattached');
}
}
})
.addEventListener('managesieve.insertrow', function(o) { bootstrap_style(o.obj); })
.addEventListener('add-recipient', function() { phone_confirmation('recipientsadded'); });
rcmail.init_pagejumper('.pagenav > input');
if (rcmail.task == 'mail') {
if (rcmail.env.action == 'compose') {
// In compose window we do not provide "Back' button, instead
// we modify the Mail button in the task menu to act like it (i.e. calls 'list' command)
if (!rcmail.env.extwin) {
$('a.mail', layout.menu).attr('onclick', "return rcmail.command('list','',this,event)");
}
rcmail.addEventListener('compose-encrypted', function(e) {
$("a.mode-html, button.attach").prop('disabled', e.active);
$('a.attach, a.responses')[e.active ? 'addClass' : 'removeClass']('disabled');
});
$('#layout-sidebar > .footer:not(.pagenav) > a.button').click(function() {
if ($(this).is('.disabled')) {
rcmail.display_message(rcmail.gettext('nocontactselected'), 'warning');
}
});
// Update compose status bar on attachments list update
if (window.MutationObserver) {
var observer, list = $('#attachment-list'),
status_callback = function() { compose_status('attach', list.children().length > 0); };
observer = new MutationObserver(status_callback);
observer.observe(list[0], {childList: true});
status_callback();
}
}
// Append contact menu to all mailto: links
if (rcmail.env.action == 'preview' || rcmail.env.action == 'show') {
$('a').filter('[href^="mailto:"]').each(function() {
mailtomenu_append(this);
});
}
}
else if (rcmail.task == 'settings') {
rcmail.addEventListener('identity-encryption-show', function(p) {
bootstrap_style(p.container);
});
rcmail.addEventListener('identity-encryption-update', function(p) {
bootstrap_style(p.container);
});
}
rcmail.set_env({
thread_padding: '1.5rem',
// increase popup windows, so they do not switch to tablet mode
popup_width_small: 1025,
popup_width: 1200
});
// Update layout after initialization (again)
// In devel mode we have to wait until all styles are applied by less
if (rcmail.env.devel_mode && window.less) {
less.pageLoadFinished.then(function() {
resize();
});
}
else {
resize();
}
// Add date format placeholder to datepicker inputs
var func, format = rcmail.env.date_format_localized;
if (format) {
func = function(input) {
$(input).filter('.datepicker').attr('placeholder', format);
// also make selects pretty
$(input).parent().find('select').each(function() { pretty_select(this); });
};
$('input.datepicker').each(function() { func(this); });
rcmail.addEventListener('insert-edit-field', func);
}
};
/**
* Apply bootstrap classes to html elements
*/
function bootstrap_style(context)
{
if (!context) {
context = document;
}
// Buttons
$('input.button,button', context).not('.btn').addClass('btn').not('.btn-primary,.primary,.mainaction').addClass('btn-secondary');
$('input.button.mainaction,button.primary,button.mainaction', context).addClass('btn-primary');
$('button.btn.delete,button.btn.discard', context).addClass('btn-danger');
$.each(['warning', 'error', 'information', 'confirmation'], function() {
var type = this;
$('.box' + type + ':not(.ui.alert)', context).each(function() {
alert_style(this, type, true);
});
});
// Convert structure of single dialogs (one input or just an image),
// e.g. group create, attachment rename where we use <label>Label<input></label>
if (context != document && $('.popup', context).children().length == 1) {
var content = $('.popup', context).children().first();
if (content.is('img')) {
$('.popup', context).addClass('justified');
}
else if (content.is('label')) {
var input = content.find('input').detach(),
label = content.detach(),
id = input.attr('id');
if (!id) {
input.attr('id', id = 'dialog-input-elastic');
}
$('.popup', context).addClass('formcontent').append(
$('<div class="form-group row">')
.append(label.attr('for', id).addClass('col-sm-2 col-form-label'))
.append($('<div class="col-sm-10">').append(input))
);
input.focus();
}
}
// Forms
var supported_controls = 'input:not(.button,.no-bs,[type=button],[type=radio],[type=checkbox]),textarea';
$(supported_controls, $('.propform', context)).addClass('form-control');
$('[type=checkbox]', $('.propform', context)).addClass('form-check-input');
// Note: On selects we add form-control to get consistent focus
// and to not have to create separate rules for selects and inputs
$('select', context).addClass('form-control custom-select');
if (context != document) {
$(supported_controls, context).addClass('form-control');
}
$('table.propform', context).each(function() {
var text_rows = 0, form_rows = 0;
$(this).find('> tbody > tr, > tr').each(function() {
var first, last, row = $(this),
row_classes = ['form-group', 'row'],
cells = row.children('td');
if (cells.length == 2) {
first = cells.first();
last = cells.last();
$('label', first).addClass('col-form-label');
first.addClass('col-sm-4');
last.addClass('col-sm-8');
if (last.find('[type=checkbox]').length == 1 && !last.find('.proplist').length) {
row_classes.push('form-check');
if (last.find('a').length) {
row_classes.push('with-link');
}
form_rows++;
}
else if (!last.find('input:not([type=hidden]),textarea,radio,select').length) {
last.addClass('form-control-plaintext');
text_rows++;
}
else {
form_rows++;
}
// style some multi-input fields
if (last.children('.datepicker') && last.children('input').length == 2) {
last.addClass('datetime');
}
}
else if (cells.length == 1) {
cells.css('width', '100%');
}
row.addClass(row_classes.join(' '));
});
if (text_rows > form_rows) {
$(this).addClass('text-only');
}
});
// Special input + anything entry
$('td.input-group', context).each(function() {
$(this).children().slice(1).addClass('input-group-append');
});
// Other forms, e.g. Contact advanced search
$('fieldset.propform:not(.groupped) div.row', context).each(function() {
var has_input = $('input:not([type=hidden]),select,textarea', this).length > 0;
if (has_input) {
$(supported_controls, this).addClass('form-control');
}
$(this).children().last().addClass('col-sm-8' + (!has_input ? ' form-control-plaintext' : ''));
$(this).children().first().addClass('col-sm-4 col-form-label');
$(this).addClass('form-group');
});
// Contact info/edit form
$('fieldset.propform.groupped fieldset', context).each(function() {
$('.row', this).each(function() {
var label, first,
has_input = $('input,select,textarea', this).length > 0,
items = $(this).children();
if (has_input) {
$(supported_controls, this).addClass('form-control');
}
if (items.length < 2) {
return;
}
first = items.first();
if (first.is('select')) {
first.addClass('input-group-prepend');
}
else {
first.wrap('<span class="input-group-prepend">').addClass('input-group-text');
}
if (!has_input) {
items.last().addClass('form-control-plaintext');
}
$('.content', this).addClass('input-group-prepend input-group-append input-group-text');
$('a.deletebutton', this).addClass('input-group-text icon delete').wrap('<span class="input-group-append">');
$(this).addClass('input-group');
});
});
// Advanced options form
$('fieldset.advanced', context).each(function() {
var table = $(this).children('.propform').first();
table.wrap($('<div>').addClass('collapse'));
$(this).children('legend').first().addClass('closed').on('click', function() {
table.parent().collapse('toggle');
$(this).toggleClass('closed');
});
});
// Other forms, e.g. Insert response
$('.propform > .prop.block:not(.row)', context).each(function() {
$(this).addClass('form-group row').each(function() {
$('label', this).addClass('col-form-label').wrap($('<div class="col-sm-4">'));
$('input,select,textarea', this).wrap($('<div class="col-sm-8">'));
$(supported_controls, this).addClass('form-control');
});
});
$('td.rowbuttons > a', context).addClass('btn');
// Testing Bootstrap Tabs on contact info/edit page
// Tabs do not scale nicely on very small screen, so can be used
// only with small number of tabs with short text labels
$('form.tabbed,div.tabbed', context).each(function(idx, item) {
var tabs = [], nav = $('<ul>').attr({'class': 'nav nav-tabs', role: 'tablist'});
$(this).addClass('tab-content').children('fieldset').each(function(i, fieldset) {
var tab, id = fieldset.id || ('tab' + idx + '-' + i),
tab_class = $(fieldset).data('navlink-class');
$(fieldset).addClass('tab-pane').attr({id: id, role: 'tabpanel'});
tab = $('<li>').addClass('nav-item').append(
$('<a>').addClass('nav-link' + (tab_class ? ' ' + tab_class : ''))
.attr({role: 'tab', 'href': '#' + id})
.text($('legend', fieldset).first().text())
.click(function(e) {
$(this).tab('show');
// Because we return false we have to close popups
popups_close(e);
// Returning false here prevents from strange scrolling issue
// when the form is in an iframe, e.g. contact edit form
return false;
})
);
$('legend', fieldset).first().hide();
tabs.push(tab);
});
// create the navigation bar
nav.append(tabs).insertBefore(item);
// activate the first tab
$('a.nav-link', nav).first().click();
});
$('input[type=file]:not(.custom-file-input)', context).each(function() {
var label_text = rcmail.gettext('choosefile' + (this.multiple ? 's' : '')),
label = $('<label>').attr({'class': 'custom-file-label',
'data-browse': rcmail.gettext('browse')}).text(label_text);
$(this).addClass('custom-file-input').wrap('<div class="custom-file">');
$(this).on('change', function() {
var text = label_text;
if (this.files.length) {
text = this.files[0].name;
if (this.files.length > 1) {
text += ', ...';
}
}
// Note: We don't use label variable to allow cloning of the input
$(this).next().text(text);
})
.parent().append(label);
});
// Make tables pretier
$('table:not(.table,.compact-table,.propform,.listing,.ui-datepicker-calendar)', context)
.filter(function() {
// exclude direct propform children and external content
return !$(this).parent().is('.propform')
&& !$(this).parents('.message-htmlpart,.message-partheaders,.boxinformation,.raw-tables').length;
})
.each(function() {
// TODO: Consider implementing automatic setting of table-responsive on window resize
var table = $(this).addClass('table');
table.parent().addClass('table-responsive-sm');
table.find('thead').addClass('thead-default');
});
// The same for some other checkboxes
// We do this here, not in setup() because we want to cover dialogs
$('input.pretty-checkbox, .propform input[type=checkbox], .form-check input, .popupmenu.form input[type=checkbox], .menu input[type=checkbox]', context)
.each(function() { pretty_checkbox(this); });
// Also when we add action-row of the form, e.g. Managesieve plugin adds them after the page is ready
if ($(context).is('.actionrow')) {
$('input[type=checkbox]', context).each(function() { pretty_checkbox(this); });
}
// Input-group combo is an element with a select field on the left
// and input(s) on right, and where the whole right side can be hidden
// depending on the select position. This code fixes border radius on select
$('.input-group-combo > select', context).first().on('change', function() {
var select = $(this),
fn = function() {
select[select.next().is(':visible') ? 'removeClass' : 'addClass']('alone');
};
setTimeout(fn, 50);
setTimeout(fn, 2000); // for devel mode
}).trigger('change');
// Make message-objects alerts pretty (the same as UI alerts)
$('#message-objects', context).children(':not(.ui.alert)').add('.part-notice').each(function() {
// message objects with notice class are really warnings
var cl = $(this).removeClass('notice part-notice').attr('class').split(/\s/)[0] || 'warning';
alert_style(this, cl);
$(this).addClass('box' + cl);
$('a', this).addClass('btn btn-primary btn-sm');
});
// Form validation errors (managesieve plugin)
$('.error', context).addClass('is-invalid');
// Make logon form prettier
if (rcmail.env.task == 'login' && context == document) {
$('#rcmloginsubmit').addClass('btn-lg text-uppercase w-100');
$('#login-form table tr').each(function() {
var input = $('input,select', this),
label = $('label', this),
icon_name = input.data('icon'),
icon = $('<i>').attr('class', 'input-group-text icon ' + input.attr('name').replace('_', ''));
if (icon_name) {
icon.addClass(icon_name);
}
$(this).addClass('form-group row');
label.parent().css('display', 'none');
input.addClass(input.is('select') ? 'custom-select' : 'form-control')
.attr('placeholder', label.text())
.before($('<span class="input-group-prepend">').append(icon))
.parent().addClass('input-group input-group-lg');
});
}
$('select:not([multiple])', context).each(function() { pretty_select(this); });
};
/**
* Detects if the element is TinyMCE dialog/menu
* and adds Elastic styling to it
*/
function tinymce_style(elem)
{
// TinyMCE dialog widnows
if ($(elem).is('.mce-window')) {
var body = $(elem).find('.mce-window-body'),
foot = $(elem).find('.mce-foot > .mce-container-body');
// Apply basic forms style
if (body.length) {
bootstrap_style(body[0]);
}
body.find('button').filter(function() { return $(this).parent('.mce-btn').length > 0; }).removeClass('btn btn-secondary');
// Fix icons in Find and Replace dialog footer
if (foot.children('.mce-widget').length === 5) {
foot.addClass('mce-search-foot');
}
// Apply some form structure fixes and helper classes
$(elem).find('.mce-charmap').parent().parent().addClass('mce-charmap-dialog');
$(elem).find('.mce-combobox').each(function() {
if (!$(this).children('.mce-btn').length) {
$(this).addClass('mce-combobox-fake');
}
});
$(elem).find('.mce-form > .mce-container-body').each(function() {
if ($(this).children('.mce-formitem').length > 4) {
$(this).addClass('mce-form-split');
}
});
$(elem).find('.mce-form').next(':not(.mce-formitem)').addClass('mce-form');
// Fix dialog height (e.g. Table properties dialog)
if (!is_mobile()) {
var offset, max_height = 0, height = body.height();
$(elem).find('.mce-form').each(function() {
max_height = Math.max(max_height, $(this).height());
});
if (height < max_height) {
max_height += (body.find('.mce-tabs').height() || 0) + 25;
body.height(max_height);
$(elem).height($(elem).height() + (max_height - height));
$(elem).css('top', ($(window).height() - $(elem).height())/2 + 'px');
}
}
}
// TinyMCE menus on mobile
else if ($(elem).is('.mce-menu')) {
$(elem).prepend(
$('<h3 class="popover-header">').append(
$('<a class="button icon "' + 'cancel' + '">')
.text(rcmail.gettext('close'))
.on('click', function() { $(document.body).click(); })));
if (window.MutationObserver) {
var callback = function() {
if (mode != 'phone') {
return;
}
if (!$('.mce-menu:visible').length) {
$('div.mce-overlay').click();
}
else if (!$('div.mce-overlay').length) {
$('<div>').attr('class', 'popover-overlay mce-overlay')
.appendTo('body')
.click(function() { $(this).remove(); });
}
};
(new MutationObserver(callback)).observe(elem, {attributes: true});
}
}
};
/**
* Initializes popup menus
*/
function dropdowns_init()
{
$('[data-popup]').each(function() { popup_init(this); });
$(document).on('click', popups_close);
rcube_webmail.set_iframe_events({mousedown: popups_close, touchstart: popups_close});
};
/**
* Init content frame
*/
function content_frame_init()
{
var last_selected = env.last_selected,
title_reset = function(title) {
if (typeof title !== 'string' || !title.length) {
title = $('h1.voice').text() || $('title').text() || '';
}
$('.header > .header-title', layout.content).text(title);
};
// display or reset the content frame
var common_content_handler = function(e, href, show, title)
{
if (is_mobile() && env.frame_nav) {
content_frame_navigation(href, e);
}
if (show && !layout.content.is(':visible')) {
env.last_selected = layout.content[0];
}
else if (!show && env.last_selected != last_selected && !env.content_lock) {
env.last_selected = last_selected;
}
screen_resize();
title_reset(title && show ? title : null);
env.content_lock = false;
};
var common_list_handler = function(e) {
if (mode != 'large' && !env.content_lock && e.force) {
show_list();
}
env.content_lock = false;
// display current folder name in list header
if (e.title) {
$('.header > .header-title', layout.list).text(e.title);
}
};
var list_handler = function(e) {
var args = {};
if (rcmail.env.task == 'addressbook' || rcmail.env.task == 'mail') {
args.force = true;
}
// display current folder name in list header
if (rcmail.env.task == 'mail' && !rcmail.env.action) {
var name = $.type(e) == 'string' ? e : rcmail.env.mailbox,
folder = rcmail.env.mailboxes[name];
args.title = folder ? folder.name : '';
}
common_list_handler(args);
};
// when loading content-frame in small-screen mode display it
layout.content.find('iframe').on('load', function(e) {
var href = '', show = true;
// Reset the scroll position of the iframe-wrapper
$(this).parent('.iframe-wrapper').scrollTop(0);
try {
href = e.target.contentWindow.location.href;
show = !href.endsWith(rcmail.env.blankpage);
// Reset title back to the default
$(e.target.contentWindow).on('unload', title_reset);
}
catch(e) { /* ignore */ }
common_content_handler(e, href, show);
});
rcmail
.addEventListener('afterlist', list_handler)
.addEventListener('afterlistgroup', list_handler)
.addEventListener('afterlistsearch', list_handler)
// plugins
.addEventListener('show-list', function(e) {
e.force = true;
common_list_handler(e);
})
.addEventListener('show-content', function(e) {
if (e.obj && !$(e.obj).is('iframe')) {
$(e.scrollElement || e.obj).scrollTop(0);
if (is_mobile()) {
iframe_loader(e.obj);
}
}
common_content_handler(e.event || new Event, '_action=' + (e.mode || 'edit'), true, e.title);
});
};
/**
* Content frame navigation
*/
function content_frame_navigation(href, event)
{
// Don't display navigation for create/add action frames
if (href.match(/_action=(create|add)/) || href.match(/_nav=hide/)) {
$(env.frame_nav).addClass('hide-nav-buttons');
return;
}
var node, uid, list, _list = $('[data-list]', layout.list).data('list');
if (!_list || !(list = rcmail[_list])) {
// hide navbar if there are no visible buttons, e.g. Help plugin UI
if ($(env.frame_nav).is('.hide-nav-buttons') && !$('.buttons', env.frame_nav).children().length) {
$(env.frame_nav).addClass('hidden');
}
return;
}
$(env.frame_nav).removeClass('hide-nav-buttons hidden');
// expand collapsed row so we do not skip the whole thread
// TODO: Unified interface for list and treelist widgets
if (uid = list.get_single_selection()) {
if (list.rows && list.rows[uid] && !list.rows[uid].expanded) {
list.expand_row(event, uid);
}
else if (list.get_node && (node = list.get_node(uid)) && node.collapsed) {
list.expand(uid);
}
}
var prev, next,
frame = $('#' + rcmail.env.contentframe),
next_button = $('a.button.next', env.frame_nav).off('click').addClass('disabled'),
prev_button = $('a.button.prev', env.frame_nav).off('click').addClass('disabled');
if ((next = list.get_next()) || rcmail.env.current_page < rcmail.env.pagecount) {
next_button.removeClass('disabled').on('click', function() {
env.content_lock = true;
iframe_loader(frame);
if (next) {
list.select(next);
}
else {
rcmail.env.list_uid = 'FIRST';
rcmail.command('nextpage');
}
});
}
if (((prev = list.get_prev()) && (prev != '*' || _list != 'subscription_list')) || rcmail.env.current_page > 1) {
prev_button.removeClass('disabled').on('click', function() {
env.content_lock = true;
iframe_loader(frame);
if (prev) {
list.select(prev);
}
else {
rcmail.env.list_uid = 'LAST';
rcmail.command('previouspage');
}
});
}
};
/**
* Handler for editor-init event
*/
function tinymce_init(o)
{
// Enable autoresize plugin
o.config.plugins += ' autoresize';
if (is_touch()) {
// Make the toolbar icons bigger
o.config.toolbar_items_size = null;
// Use minimalistic toolbar
o.config.toolbar = 'undo redo | insert | styleselect';
if (o.config.plugins.match(/emoticons/)) {
o.config.toolbar += ' emoticons';
}
}
if (rcmail.task == 'mail' && rcmail.env.action == 'compose') {
var form = $('#compose-content > form'),
keypress = function(e) {
if (e.key == 'Tab' && e.shiftKey) {
$('#compose-content > form').scrollTop(0);
}
};
// Shift+Tab on mail compose editor scrolls the page to the top
o.config.setup_callback = function(ed) {
ed.on('keypress', keypress);
};
$('#composebody').on('keypress', keypress);
// Keep the editor toolbar on top of the screen on scroll
form.on('scroll', function() {
var container = $('.mce-container-body', form),
toolbar = $('.mce-top-part', container),
editor_offset = container.offset(),
header_top = form.offset().top;
if (editor_offset && (editor_offset.top - header_top < 0)) {
toolbar.css({position: 'fixed', top: header_top + 'px', width: container.width() + 'px'});
}
else {
toolbar.css({position: 'relative', top: 0, width: 'auto'})
}
});
$(window).resize(function() { form.trigger('scroll'); });
}
};
function datepicker_init(datepicker)
{
// Datepicker widget improvements: overlay element, styling updates on calendar element update
// The widget does not provide any event system, so we use MutationObserver
if (window.MutationObserver) {
$(datepicker).not('[data-observed]').each(function() {
var overlay, hidden = true,
win = is_framed ? parent : window,
callback = function(data) {
$.each(data, function(i, v) {
// add/remove overlay on widget show/hide
if (v.type == 'attributes') {
var is_hidden = $(v.target).attr('aria-hidden') == 'true';
if (is_hidden != hidden) {
if (!is_hidden) {
overlay = $('<div>').attr('class', 'ui-widget-overlay datepicker')
.appendTo(win.document.body)
.click(function(e) {
$(this).remove();
if (is_framed) {
$.datepicker._hideDatepicker();
}
});
}
else if (overlay) {
overlay.remove();
}
hidden = is_hidden;
}
}
else if (v.addedNodes.length) {
// apply styles when widget content changed
win.UI.bootstrap_style(v.target);
// Month/Year change handlers do not work from parent, fix it
if (is_framed) {
win.$('select.ui-datepicker-month', v.target).on('change', function() {
$.datepicker._selectMonthYear($.datepicker._lastInput, this, "M");
});
win.$('select.ui-datepicker-year', v.target).on('change', function() {
$.datepicker._selectMonthYear($.datepicker._lastInput, this, "Y");
});
}
}
});
};
$(this).attr('data-observed', '1');
if (is_framed) {
// move the datepicker to parent window
$(this).detach().appendTo(parent.document.body);
// create fake element, so the valid one is not removed by datepicker code
$('<div id="ui-datepicker-div" class="hidden">').appendTo(document.body);
}
(new MutationObserver(callback)).observe(this, {childList: true, subtree: false, attributes: true, attributeFilter: ['aria-hidden']});
});
}
};
/**
* Handler for some Roundcube core popups
*/
function rcmail_popup_init(o)
{
// Add some common styling to the autocomplete/googiespell popups
$('ul', o.obj).addClass('menu listing iconized');
$(o.obj).addClass('popupmenu popover');
bootstrap_style(o.obj);
// for googiespell list
$('input', o.obj).addClass('form-control');
// Modify the googiespell menu on mobile
if (is_mobile() && $(o.obj).is('.googie_window')) {
// Set popup Close title
var title = rcmail.gettext('close'),
class_name = 'button icon cancel',
close_link = $('<a>').attr('class', class_name).text(title)
.click(function(e) {
e.stopPropagation();
$('.popover-overlay').remove();
$(o.obj).hide();
});
$('<h3 class="popover-header">').append(close_link).prependTo(o.obj);
// add overlay element for phone layout
if (!$('.popover-overlay').length) {
$('<div>').attr('class', 'popover-overlay')
.appendTo('body')
.click(function() { $(this).remove(); });
}
$('ul,button', o.obj).click(function(e) {
if (!$(e.target).is('input')) {
$('.popover-overlay').remove();
}
});
}
};
/**
* Handler for 'enable-command' event
*/
function enable_command_handler(args)
{
if (is_framed) {
$.each(frame_buttons, function(i, button) {
if (args.command == button.command) {
parent.$('#' + button.button_id)[args.status ? 'removeClass' : 'addClass']('disabled');
}
});
}
if (rcmail.task == 'mail') {
switch (args.command) {
case 'reply-list':
if (rcmail.env.reply_all_mode == 1) {
var label = rcmail.gettext(args.status ? 'replylist' : 'replyall');
$('a.button.reply-all').attr('title', label).find('.inner').text(label);
}
break;
case 'compose-encrypted':
// show the toolbar button for Mailvelope
if (args.status) {
$('a.button.encrypt:not(.icon)').parent().show();
}
break;
case 'compose-encrypted-signed':
// enable selector for encrypt and sign
$('#encryption-menu-button').show();
break;
}
}
};
/**
* screen mode
*/
function screen_mode()
{
var size, width = $(window).width();
if (width <= 480)
size = 'phone';
else if (width > 1200)
size = 'large';
else if (width > 768)
size = 'normal';
else
size = 'small';
touch = width <= 1024;
mode = size;
};
/**
* Window resize handler
* Does layout reflows e.g. on screen orientation change
*/
function resize()
{
var mobile;
screen_mode();
screen_resize();
screen_resize_html();
// disable ext-windows and other features
if (mobile = is_mobile()) {
rcmail.set_env(env.small_screen_config);
rcmail.enable_command('extwin', false);
}
else {
rcmail.set_env(env.config);
rcmail.enable_command('extwin', true);
}
// Hide content frame buttons on small devices (with frame toolbar in parent window)
$.each(content_buttons, function() { $(this)[mobile ? 'hide' : 'show'](); });
};
function screen_resize()
{
if (is_framed && !layout.sidebar.length && !layout.list.length) {
screen_resize_headers();
return;
}
switch (mode) {
case 'phone': screen_resize_phone(); break;
case 'small': screen_resize_small(); break;
case 'normal': screen_resize_normal(); break;
case 'large': screen_resize_large(); break;
}
screen_resize_logo(mode);
screen_resize_headers();
// On iOS and Android the content frame height is never correct, fix it.
// Actually I observed the issue on my old iPad with iOS 9.3.
if (bw.webkit && bw.ipad && bw.agent.match(/OS 9/)) {
$('.iframe-wrapper').each(function() {
var h = $(this).height();
if (h) {
$(this).children('iframe').height(h);
}
});
}
};
/**
* Assigns layout-* and touch-mode class to the 'html' element
*
* If we're inside an iframe that is small we have to
* check if the parent window is also small (mobile).
* We use that e.g. to still display desktop-like popovers in dialogs
*/
function screen_resize_html()
{
var meta = layout_metadata(),
html = $(document.documentElement);
if (html[0].className.match(/layout-([a-z]+)/)) {
if (RegExp.$1 != meta.mode) {
html.removeClass('layout-' + RegExp.$1)
.addClass('layout-' + meta.mode);
}
}
else {
html.addClass('layout-' + meta.mode);
}
if (meta.touch && !html.is('.touch')) {
html.addClass('touch');
}
else if (!meta.touch && html.is('.touch')) {
html.removeClass('touch');
}
};
function screen_resize_logo(mode)
{
if (mode == 'phone' && $('#logo').data('src-small')) {
$('#logo').attr('src', $('#logo').data('src-small'));
}
else {
$('#logo').attr('src', $('#logo').data('src-default'));
}
}
/**
* Sets left and right margin to the header title element to make it
* properly centered depending on the number of buttons on both sides
*/
function screen_resize_headers()
{
$('#layout > div > .header').each(function() {
var title, right = 0, left = 0, padding = 0,
sizes = {left: 0, right: 0};
$(this).children(':visible').each(function() {
if (!title && $(this).is('.header-title')) {
title = $(this);
return;
}
sizes[title ? 'right' : 'left'] += this.offsetWidth;
});
if (padding + sizes.right >= sizes.left) {
right = 0;
left = sizes.right + padding - sizes.left;
}
else {
left = 0;
right = sizes.left - (padding + sizes.right);
}
$(title).css({
'margin-right': right + 'px',
'margin-left': left + 'px',
'padding-right': padding + 'px'
});
});
};
function screen_resize_phone()
{
screen_resize_small_all();
app_menu(false);
};
function screen_resize_small()
{
screen_resize_small_all();
app_menu(true);
};
function screen_resize_normal()
{
var show;
if (layout.list.length) {
show = layout.list.is(env.last_selected) || (!layout.sidebar.is(env.last_selected) && !layout.sidebar.is('.layout-sticky'));
layout.list[show ? 'removeClass' : 'addClass']('hidden');
}
if (layout.sidebar.length) {
show = !layout.list.length || layout.sidebar.is(env.last_selected) || layout.sidebar.is('.layout-sticky');
layout.sidebar[show ? 'removeClass' : 'addClass']('hidden');
}
layout.content.removeClass('hidden');
app_menu(true);
screen_resize_small_none();
if (layout.list) {
$('.header > ul.menu', layout.list).addClass('popupmenu');
}
};
function screen_resize_large()
{
$.each(layout, function(name, item) { item.removeClass('hidden'); });
screen_resize_small_none();
if (layout.list) {
$('.header > ul.menu.popupmenu', layout.list).removeClass('popupmenu');
}
};
function screen_resize_small_all()
{
var show, got_content = false;
if (layout.content.length) {
show = got_content = layout.content.is(env.last_selected);
layout.content[show ? 'removeClass' : 'addClass']('hidden');
$('.header > ul.menu', layout.content).addClass('popupmenu');
}
if (layout.list.length) {
show = !got_content && layout.list.is(env.last_selected);
layout.list[show ? 'removeClass' : 'addClass']('hidden');
$('.header > ul.menu', layout.list).addClass('popupmenu');
}
if (layout.sidebar.length) {
show = !got_content && (layout.sidebar.is(env.last_selected) || !layout.list.length);
layout.sidebar[show ? 'removeClass' : 'addClass']('hidden');
}
if (got_content) {
buttons.back_list.show();
}
};
function screen_resize_small_none()
{
buttons.back_list.filter(function() { return $(this).parents('#layout-sidebar').length == 0; }).hide();
$('ul.menu.popupmenu').removeClass('popupmenu');
};
function show_content(unsticky)
{
// show sidebar and hide list
layout.list.addClass('hidden');
layout.sidebar.addClass('hidden');
layout.content.removeClass('hidden');
if (unsticky) {
layout.sidebar.removeClass('layout-sticky');
}
screen_resize_headers();
env.last_selected = layout.content[0];
};
function show_sidebar(sticky)
{
// show sidebar and hide list
layout.list.addClass('hidden');
layout.sidebar.removeClass('hidden');
if (sticky) {
layout.sidebar.addClass('layout-sticky');
}
if (mode == 'small' || mode == 'phone') {
layout.content.addClass('hidden');
}
screen_resize_headers();
env.last_selected = layout.sidebar[0];
};
function show_list(scroll)
{
if (!layout.list.length && !layout.sidebar.length) {
history.back();
}
else {
// show list and hide sidebar and content
layout.sidebar.addClass('hidden').removeClass('layout-sticky');
layout.list.removeClass('hidden');
if (mode == 'small' || mode == 'phone') {
hide_content();
}
if (scroll) {
layout.list.children('.scroller').scrollTop(0);
}
env.last_selected = layout.list[0];
}
screen_resize_headers();
};
function hide_content()
{
// show sidebar or list, hide content frame
env.last_selected = layout.list[0] || layout.sidebar[0];
screen_resize();
// reset content frame, so we can load it again
rcmail.show_contentframe(false);
// now we have to unselect selected row on the list
$('[data-list]', layout.list).each(function() {
var list = $(this).data('list');
if (rcmail[list]) {
if (rcmail[list].clear_selection) {
rcmail[list].clear_selection(); // list widget
}
else if (rcmail[list].select) {
rcmail[list].select(); // treelist widget
}
}
});
};
// show menu widget
function app_menu(show)
{
if (show) {
if (mode == 'phone') {
$('<div id="menu-overlay" class="popover-overlay">')
.on('click', function() { app_menu(false); })
.appendTo('body');
if (!env.menu_initialized) {
env.menu_initialized = true;
$('a', layout.menu).on('click', function(e) { if (mode == 'phone') app_menu(); });
}
layout.menu.addClass('popover');
}
layout.menu.removeClass('hidden');
}
else {
$('#menu-overlay').remove();
layout.menu.addClass('hidden').removeClass('popover');
}
};
/**
* Triggered when a UI message is displayed
*/
function message_displayed(p)
{
if (p.type == 'loading' && $('.iframe-loader:visible').length) {
// hide original message object, we don't need two "loaders"
rcmail.hide_message(p.object);
return;
}
alert_style(p.object, p.type, true);
$(p.object).attr('role', 'alert');
};
/**
* Applies some styling and icon to an alert object
*/
function alert_style(object, type, wrap)
{
var tmp, classes = 'ui alert',
addicon = !$(object).is('.noicon'),
map = {
information: 'alert-info',
notice: 'alert-info',
confirmation: 'alert-success',
warning: 'alert-warning',
error: 'alert-danger',
loading: 'alert-info loading',
uploading: 'alert-info loading',
vcardattachment: 'alert-info' // vcard_attachments plugin
};
// we need the content to be non-text node for best alignment
if (wrap && addicon && !$(object).is('.aligned-buttons')) {
$(object).html($('<span>').html($(object).html()));
}
// Type can be e.g. 'notice chat'
type = type.split(' ')[0];
if (tmp = map[type]) {
classes += ' ' + tmp;
if (addicon) {
$('<i>').attr('class', 'icon').prependTo(object);
}
}
$(object).addClass(classes);
};
/**
* Set UI dialogs size/style depending on screen size
*/
function dialog_open(dialog)
{
var me = $(dialog.uiDialog),
width = me.width(),
height = me.height(),
maxWidth = $(window).width(),
maxHeight = $(window).height();
if (maxWidth <= 480) {
me.css({width: '100%', height: '100%'});
}
else {
if (height > maxHeight) {
me.css('height', '100%');
}
if (width > maxWidth) {
me.css('width', '100%');
}
}
// Close all popovers
$(document).click();
// Display loader when the dialog has an iframe
iframe_loader($('div.popup > iframe', me));
// TODO: style buttons/forms
bootstrap_style(dialog.uiDialog);
};
/**
* Initializes searchbar widget
*/
function searchbar_init(bar)
{
var options_button = $('a.button.options', bar),
input = $('input:not([type=hidden])', bar),
placeholder = input.attr('placeholder'),
form = $('form', bar),
is_search_pending = function() {
if (input.val()) {
return true;
}
if (rcmail.task == 'mail' && $('#s_interval').val()) {
return true;
}
if (rcmail.gui_objects.search_filter && $(rcmail.gui_objects.search_filter).val() != 'ALL') {
return true;
}
if (rcmail.gui_objects.foldersfilter && $(rcmail.gui_objects.foldersfilter).val() != '---') {
return true;
}
},
close_func = function() {
if ($(bar).is('.open')) {
options_button.click();
}
},
update_func = function() {
$(bar)[is_search_pending() ? 'addClass' : 'removeClass']('active');
};
options_button.on('click', function(e) {
var id = $(this).data('target'),
options = $('#' + id),
open = options.is(':visible');
if (options.length) {
if (!open) {
if (ref[id]) {
ref[id](options.get(0), this, e);
}
else if (typeof window[id] == 'function') {
window[id](options.get(0), this, e);
}
}
options.next()[open ? 'show' : 'hide']();
options.toggleClass('hidden');
$('.floating-action-buttons').toggleClass('hidden');
$(bar).toggleClass('open');
$('button.search', options).off('click.search').on('click.search', function() {
options_button.trigger('click');
update_func();
});
}
});
input.on('input change', update_func)
.on('focus blur', function(e) { input.attr('placeholder', e.type == 'blur' ? placeholder : ''); });
// Search reset action
$('a.reset', bar).on('click', function(e) {
// for treelist widget's search setting val and keyup.treelist is needed
// in normal search form reset-search command will do the trick
input.val('').change().trigger('keyup.treelist', {keyCode: 27});
if ($(bar).is('.open')) {
options_button.click();
}
// Reset filter
if (rcmail.gui_objects.search_filter) {
$(rcmail.gui_objects.search_filter).val('ALL');
}
if (rcmail.gui_objects.foldersfilter) {
$(rcmail.gui_objects.foldersfilter).val('---').change();
rcmail.folder_filter('---');
}
update_func();
});
rcmail.addEventListener('init', update_func)
.addEventListener('responsebeforesearch', update_func)
// close options form on list/search request
.addEventListener('beforelist', close_func)
.addEventListener('beforesearch', close_func);
};
/**
* Converts toolbar menu into popup-menu for small screens
*/
function toolbar_init()
{
if (env.got_smart_toolbar) {
return;
}
env.got_smart_toolbar = true;
var list_mark, items = [],
list_items = [],
meta = layout_metadata(),
button_func = function(button, items, cloned) {
var item = $('<li role="menuitem">'),
button = cloned ? create_cloned_button($(button), true, 'hidden-big hidden-large') : $(button).detach();
// Remove empty text nodes that break alignment of text of the menu item
button.contents().filter(function() { if (this.nodeType == 3 && !$.trim(this.nodeValue).length) $(this).remove(); });
if (button.is('.spacer')) {
item.addClass('spacer');
}
else {
item.append(button);
}
items.push(item);
};
// convert content toolbar to a popup list
if (layout.content) {
$('.header > .menu', layout.content).each(function() {
var toolbar = $(this);
toolbar.children().each(function() { button_func(this, items); });
toolbar.remove();
});
}
// convert list toolbar to a popup list
if (layout.list) {
$('.header > .menu', layout.list).each(function() {
var toolbar = $(this);
list_mark = toolbar.next();
toolbar.children().each(function() {
if (meta.mode != 'large') {
// TODO: Would be better to set this automatically on submenu display
// i.e. in show/shown event (see popup_init()), if possible
$(this).data('popup-pos', 'right');
}
// add items to the content menu too
button_func(this, items, true);
button_func(this, list_items);
});
toolbar.remove();
});
}
// special elements to clone and add to the toolbar (mobile only)
$('ul[data-menu="toolbar-small"] > li > a').each(function() {
var button = $(this).clone();
button.attr('id', this.id + '_clone');
// TODO: rcmail.register_button()
items.push($('<li role="menuitem">').addClass('hidden-big').append(button));
});
// append the new list toolbar and menu button
if (list_items.length) {
var container = layout.list.children('.header'),
menu_attrs = {'class': 'menu toolbar popupmenu listing iconized', id: 'toolbar-list-menu'},
menu_button = $('<a class="button icon toolbar-list-button" href="#list-menu">')
.attr({'data-popup': 'toolbar-list-menu'}),
// TODO: copy original toolbar attributes (class, role, aria-*)
toolbar = $('<ul>').attr(menu_attrs).data('popup-parent', container).append(list_items);
if (list_mark.length) {
toolbar.insertBefore(list_mark);
}
else {
container.append(toolbar);
}
container.append(menu_button);
}
// append the new toolbar and menu button
if (items.length) {
var container = layout.content.children('.header'),
menu_attrs = {'class': 'menu toolbar popupmenu listing iconized', id: 'toolbar-menu'},
menu_button = $('<a class="button icon toolbar-menu-button" href="#menu">')
.attr({'data-popup': 'toolbar-menu'});
container
// TODO: copy original toolbar attributes (class, role, aria-*)
.append($('<ul>').attr(menu_attrs).data('popup-parent', container).append(items))
.append(menu_button);
if (layout.list.length) {
// bind toolbar menu with the menu button in the list header
$('a.toolbar-menu-button', layout.list).click(function(e) {
e.stopPropagation();
menu_button.click();
});
}
}
};
/**
* Initialize a popup for specified button element
*/
function popup_init(item, win)
{
// On mobile we display the menu from the frame in the parent window
if (is_framed && is_mobile()) {
return parent.UI.popup_init(item, win || window);
}
if (!win) win = window;
var level,
popup_id = $(item).data('popup'),
popup = $(win.$('#' + popup_id).get(0)), // a "hack" to support elements in frames
popup_orig = popup,
title = $(item).attr('title'),
content_element = function() {
// On mobile we display a menu from the frame in the parent window
// To make menu actions working we have to clone the menu
// and pass click events to it...
if (win != window) {
popup = popup_orig.clone(true, true);
popup.attr('id', popup_id + '-clone')
.appendTo(document.body)
.find('li > a').attr('onclick', '').off('click').on('click', function(e) {
if (!$(this).is('.disabled')) {
$(item).popover('hide');
win.$('#' + $(this).attr('id')).click();
}
return false;
});
}
return popup.get(0);
};
$(item).attr({
'aria-haspopup': 'true',
'aria-expanded': 'false',
'aria-owns': popup_id,
})
.popover({
content: content_element,
trigger: $(item).data('popup-trigger') || 'click',
placement: $(item).data('popup-pos') || 'bottom',
animation: true,
boundary: 'window', // fix for https://github.com/twbs/bootstrap/issues/25428
html: true
})
.on('show.bs.popover', function(event) {
var init_func = popup.data('popup-init');
if (popup_id && menus[popup_id]) {
menus[popup_id].transitioning = true;
}
if (init_func && ref[init_func]) {
ref[init_func](popup.get(0), item, event);
}
else if (init_func && win[init_func]) {
win[init_func](popup.get(0), item, event);
}
level = $('div.popover:visible').length + 1;
popup.removeClass('hidden').attr('aria-hidden', false)
// Stop propagation on menu items that have popups
// to make a click on them not hide their parent menu(s)
.find('[aria-haspopup="true"]')
.data('level', level + 1)
.off('click.popup')
.on('click.popup', function(e) { e.stopPropagation(); });
if (!is_mobile()) {
// Set popup height so it is less than the window height
popup.css('max-height', Math.min(36 * 15 - 1, $(window).height() - 30));
}
})
.on('shown.bs.popover', function(event) {
var mobile = is_mobile(),
popover = $('#' + $(item).attr('aria-describedby'));
level = $(item).data('level') || 1;
// Set popup Back/Close title
if (mobile) {
var label = level > 1 ? 'back' : 'close',
title = rcmail.gettext(label),
class_name = 'button icon ' + (label == 'back' ? 'back' : 'cancel');
$('.popover-header', popover).empty()
.append($('<a>').attr('class', class_name).text(title)
.on('click', function(e) {
$(item).popover('hide');
if (level > 1) {
e.stopPropagation();
}
})
.on('mousedown', function(e) {
// stop propagation to i.e. do not close jQuery-UI dialogs below
e.stopPropagation();
})
);
}
// Hide other menus on the same level
$.each(menus, function(id, prop) {
if ($(prop.target).data('level') == level && id != popup_id) {
menu_hide(id);
}
});
// On keyboard event focus the first (active) entry and enable keyboard navigation
if ($(item).data('event') == 'key') {
popover.off('keydown.popup').on('keydown.popup', 'a.active', function(e) {
var entry, node, mode = 'next';
switch (e.which) {
case 27: // ESC
case 9: // TAB
$(item).popover('toggle').focus();
return false;
case 38: // ARROW-UP
case 63232:
mode = 'previous';
case 40: // ARROW-DOWN
case 63233:
entry = e.target.parentNode;
while (entry = entry[mode + 'Sibling']) {
if (node = $(entry).children('.active')[0]) {
node.focus();
break;
}
}
return false; // prevents from scrolling the whole page
}
});
popover.find('a.active').first().focus();
}
if (popup_id && menus[popup_id]) {
menus[popup_id].transitioning = false;
}
// add overlay element for phone layout
if (mobile && !$('.popover-overlay').length) {
$('<div>').attr('class', 'popover-overlay')
.appendTo('body')
.click(function() { $(this).remove(); });
}
$('.popover-body', popover).addClass('webkit-scroller');
})
.on('hide.bs.popover', function() {
if (level == 1) {
$('.popover-overlay').remove();
}
if (popup_id && menus[popup_id] && popup.is(':visible')) {
menus[popup_id].transitioning = true;
}
})
.on('hidden.bs.popover', function() {
if (/-clone$/.test(popup.attr('id'))) {
popup.remove();
}
else {
popup.attr('aria-hidden', true)
// Some menus aren't being hidden, force that
.addClass('hidden')
// Bootstrap will detach the popup element from
// the DOM (https://github.com/twbs/bootstrap/issues/20219)
// making our menus to not update buttons state.
// Work around this by attaching it back to the DOM tree.
popup.appendTo(popup.data('popup-parent') || document.body);
}
// close orphaned popovers, for some reason there are sometimes such dummy elements left
$('.popover-body:empty').each(function() { $(this).parent().remove(); });
if (popup_id && menus[popup_id]) {
delete menus[popup_id];
}
})
// Because Bootstrap does not provide originalEvent in show/shown events
// we have to handle that by our own using click and keydown handlers
.on('click', function() {
$(this).data('event', 'mouse');
})
.on('keydown', function(e) {
if (e.originalEvent) {
switch (e.originalEvent.which) {
case 13:
case 32:
// Open the popup on ENTER or SPACE
e.preventDefault();
$(this).data('event', 'key').popover('toggle');
break;
case 27:
// Close the popup on ESC key
$(this).popover('hide');
break;
}
}
});
// re-add title attribute removed by bootstrap popover
if (title) {
$(item).attr('title', title);
}
popup.attr('aria-hidden', 'true').data('button', item);
// stop propagation to e.g. do not hide the popup when
// clicking inside on form elements
if (popup.data('editable')) {
popup.on('click mousedown', function(e) { e.stopPropagation(); });
}
};
/**
* Closes all popups (for use as event handler)
*/
function popups_close(e)
{
// Ignore some of propagated click events (see pretty_select())
if (popups_close_lock && popups_close_lock > (new Date().getTime() - 250)) {
return;
}
$('.popover.show').each(function() {
var popup = $('.popover-body', this),
button = popup.children().first().data('button');
if (button && e.target != button && !$(button).find(e.target).length && typeof button !== 'string') {
$(button).popover('hide');
}
if (!button) {
$(this).remove();
}
});
};
/**
* Handler for menu-open and menu-close events
*/
function menu_toggle(p)
{
if (!p || !p.name || (p.props && p.props.skinable === false)) {
return;
}
if (is_framed && is_mobile()) {
if (!p.win) {
p.win = window;
}
return parent.UI.menu_toggle(p);
}
if (p.name == 'messagelistmenu') {
menu_messagelist(p);
}
else if (p.event == 'menu-open') {
var fn, pos,
content = $('ul', p.obj).first(),
target = p.props && p.props.link ? p.props.link : p.originalEvent.target;
if ($(target).is('span')) {
target = $(target).parents('a,li')[0];
}
if (p.name.match(/^drag/)) {
// create a fake element to position drag menu on the cursor position
pos = rcube_event.get_mouse_pos(p.originalEvent);
target = $('<a>').css({
position: 'absolute',
left: pos.x,
top: pos.y,
height: '1px',
width: '1px',
visibility: 'hidden'
})
.appendTo(document.body).get(0);
}
pos = $(target).data('popup-pos') || 'right';
if (p.name == 'folder-selector') {
content.addClass('listing folderlist');
}
else if (p.name == 'addressbook-selector' || p.name == 'contactgroup-selector') {
content.addClass('listing contactlist');
}
else if (content.hasClass('menu')) {
content.addClass('listing');
}
if (p.name == 'pagejump-selector') {
content.addClass('simplelist');
p.obj.addClass('simplelist');
pos = 'top';
}
// There can be only one menu of the same type
if (menus[p.name]) {
menu_hide(p.name, p.originalEvent);
}
// Popover menus use animation. Sometimes the same menu is
// immediately hidden and shown (e.g. folder-selector for copy and move action)
// we have to wait until the previous menu hides before we can open it again
fn = function() {
if (menus[p.name] && menus[p.name].transitioning) {
return setTimeout(fn, 50);
}
if (!$(target).data('popup')) {
$(target).data({
event: rcube_event.is_keyboard(p.originalEvent) ? 'key' : 'mouse',
popup: p.name,
'popup-pos': pos,
'popup-trigger': 'manual'
});
popup_init(target, p.win);
}
menus[p.name] = {target: target};
$(target).popover('show');
}
fn();
}
else {
menu_hide(p.name, p.originalEvent);
}
// Stop propagation so multi-level menus work properly
p.originalEvent.stopPropagation();
};
/**
* Close menu by name
*/
function menu_hide(name, event)
{
var target = menu_target(name);
if (name.match(/^drag/)) {
$(target).popover('dispose').remove();
}
else {
$(target).popover('hide');
// In phone mode close all menus when forwardmenu is requested to be closed
// FIXME: This is a hack, we need some generic solution.
if (name == 'forwardmenu') {
popups_close(event);
}
}
};
/**
* Destroys menu by name
*
* This is required when you replace the menu content element
*/
function menu_destroy(name)
{
$('[aria-owns=' + name + ']').popover('dispose').data('popup', null);
};
/**
* Get menu target by name
*/
function menu_target(name)
{
var target;
if (menus[name]) {
target = menus[name].target;
}
else {
target = $('#' + name).data('button');
if (!target) {
// catch cases as 'forwardmenu' where menu suffix has no hyphen
// or try with -menu suffix if it's not in the menu name already
if (name.match(/(?!-)menu$/)) {
name = name.substr(0, name.length - 4);
}
target = $('#' + name + '-menu').data('button');
}
}
return target;
};
/**
* Messages list options dialog
*/
function menu_messagelist(p)
{
var content = $('#listoptions-menu'),
width = content.width() + 25,
dialog = content.clone(true);
// set form values
$('select[name="sort_col"]', dialog).val(rcmail.env.sort_col || '');
$('select[name="sort_ord"]', dialog).val(rcmail.env.sort_order || 'ASC');
$('select[name="mode"]', dialog).val(rcmail.env.threading ? 'threads' : 'list');
// Fix id/for attributes
$('select', dialog).each(function() { this.id = this.id + '-clone'; });
$('label', dialog).each(function() { $(this).attr('for', $(this).attr('for') + '-clone'); });
var save_func = function(e) {
if (rcube_event.is_keyboard(e.originalEvent)) {
$('#listmenulink').focus();
}
var col = $('select[name="sort_col"]', dialog).val(),
ord = $('select[name="sort_ord"]', dialog).val(),
mode = $('select[name="mode"]', dialog).val();
rcmail.set_list_options([], col, ord, mode == 'threads' ? 1 : 0);
return true;
};
dialog = rcmail.simple_dialog(dialog, rcmail.gettext('listoptionstitle'), save_func, {
closeOnEscape: true,
minWidth: 400
});
};
/**
* About dialog
*/
function about_dialog(elem)
{
var support_url, support_func, support_button = false,
dialog = $('<iframe>').attr({id: 'aboutframe', src: rcmail.url('settings/about', {_framed: 1})}),
support_link = $('#supportlink');
if (support_link.length && (support_url = support_link.attr('href'))) {
support_button = support_link.text();
support_func = function(e) { support_url.indexOf('mailto:') < 0 ? window.open(support_url) : location.href = support_url; };
}
rcmail.simple_dialog(dialog, $(elem).text(), support_func, {
button: support_button,
button_class: 'help',
cancel_button: 'close',
height: 400
});
};
/**
* Show/hide more mail headers (envelope)
*/
function headers_show(button)
{
var headers = $(button).parent().prev();
headers[headers.is('.hidden') ? 'removeClass' : 'addClass']('hidden');
};
/**
* Mail headers dialog
*/
function headers_dialog()
{
var props = {_uid: rcmail.env.uid, _mbox: rcmail.env.mailbox, _framed: 1},
dialog = $('<iframe>').attr({id: 'headersframe', src: rcmail.url('headers', props)});
rcmail.simple_dialog(dialog, rcmail.gettext('arialabelmessageheaders'), null, {
cancel_button: 'close',
height: 400
});
};
/**
* Mail import dialog
*/
function import_dialog()
{
if (!rcmail.commands['import-messages']) {
return;
}
var content = $('#uploadform'),
dialog = content.clone(true);
var save_func = function(e) {
return rcmail.command('import-messages', $(dialog.find('form')[0]));
};
rcmail.simple_dialog(dialog, rcmail.gettext('importmessages'), save_func, {
button: 'import',
closeOnEscape: true,
minWidth: 400
});
};
/**
* Search options menu popup
*/
function searchmenu(obj)
{
var n, all,
list = $('input[name="s_mods[]"]', obj),
scope_select = $('#s_scope', obj),
mbox = rcmail.env.mailbox,
mods = rcmail.env.search_mods,
scope = rcmail.env.search_scope || 'base';
if (!$(obj).data('initialized')) {
$(obj).data('initialized', true);
if (list.length) {
list.on('change', function() { set_searchmod(obj, this); });
rcmail.addEventListener('beforesearch', function() { set_searchmod(obj); });
}
}
if (rcmail.env.search_mods) {
if (rcmail.env.task == 'mail') {
if (scope == 'all') {
mbox = '*';
}
mods = mods[mbox] ? mods[mbox] : mods['*'];
all = 'text';
scope_select.val(scope);
}
else {
all = '*';
}
if (mods[all]) {
list.map(function() {
this.checked = true;
this.disabled = this.value != all;
});
}
else {
list.prop('disabled', false).prop('checked', false);
for (n in mods) {
list.filter('[value="' + n + '"]').prop('checked', true);
}
}
}
};
function set_searchmod(menu, elem)
{
var all, m, task = rcmail.env.task,
mods = rcmail.env.search_mods,
mbox = rcmail.env.mailbox,
scope = $('#s_scope', menu).val(),
interval = $('#s_interval', menu).val();
if (scope == 'all') {
mbox = '*';
}
if (!mods) {
mods = {};
}
if (task == 'mail') {
if (!mods[mbox]) {
mods[mbox] = rcube_clone_object(mods['*']);
}
m = mods[mbox];
all = 'text';
rcmail.env.search_scope = scope;
rcmail.env.search_interval = interval;
}
else { //addressbook
m = mods;
all = '*';
}
if (!elem) {
return;
}
if (!elem.checked) {
delete(m[elem.value]);
}
else {
m[elem.value] = 1;
}
// mark all fields
if (elem.value == all) {
$('input[name="s_mods[]"]', menu).map(function() {
if (this == elem) {
return;
}
this.checked = true;
if (elem.checked) {
this.disabled = true;
delete m[this.value];
}
else {
this.disabled = false;
m[this.value] = 1;
}
});
}
rcmail.set_searchmods(m);
};
/**
* Spellcheck languages list
*/
function spellmenu(obj)
{
var i, link, li, list = [],
lang = rcmail.spellcheck_lang(),
ul = $('ul', obj);
if (!ul.length) {
ul = $('<ul class="selectable listing iconized" role="menu">');
for (i in rcmail.env.spell_langs) {
li = $('<li role="menuitem">');
link = $('<a href="#'+ i +'" tabindex="0"></a>')
.text(rcmail.env.spell_langs[i])
.addClass('active').data('lang', i)
.on('click keypress', function(e) {
if (e.type != 'keypress' || rcube_event.get_keycode(e) == 13) {
rcmail.spellcheck_lang_set($(this).data('lang'));
rcmail.hide_menu('spell-menu', e);
return false;
}
});
link.appendTo(li);
list.push(li);
}
ul.append(list).appendTo(obj);
}
// select current language
$('li', ul).each(function() {
var el = $('a', this);
if (el.data('lang') == lang) {
el.addClass('selected').attr('aria-selected', 'true');
}
else if (el.hasClass('selected')) {
el.removeClass('selected').removeAttr('aria-selected');
}
});
};
/**
* Add/remove item to/from compose options status bar
*/
function compose_status(id, status)
{
var bar = $('#composestatusbar'), ico = bar.find('a.button.icon.' + id);
if (!status) {
ico.remove();
}
else if (!ico.length) {
$('<a>').attr('class', 'button icon ' + id)
.on('click', function() { show_sidebar(); })
.appendTo(bar);
}
};
/**
* Attachment menu
*/
function attachmentmenu(obj, button, event)
{
var id = $(button).parent().attr('id').replace(/^attach/, '');
$.each(['open', 'download', 'rename'], function() {
var action = this;
$('#attachmenu' + action, obj).off('click').attr('onclick', '').click(function(e) {
return rcmail.command(action + '-attachment', id, this, e.originalEvent);
});
});
// call menu-open so core can set state of menu commands
return rcmail.command('menu-open', {menu: 'attachmentmenu', id: id}, obj, event);
};
/**
* Appends drop-icon to attachments list item (to invoke attachment menu)
*/
function attachmentmenu_append(item)
{
item = $(item);
- if (!item.is('.no-menu') && !item.children('.drop').length) {
- var label = rcmail.gettext('options');
- var button = $('<a>')
- .attr({
+ if (!item.is('.no-menu') && !item.children('.dropdown').length) {
+ var label = rcmail.gettext('options'),
+ fname = item.find('a.filename');
+
+ var button = $('<a>').attr({
href: '#',
- tabindex: 0,
+ tabindex: fname.attr('tabindex') || 0,
title: label,
'class': 'button icon dropdown skip-content'
})
.on('click', function(e) {
return attachmentmenu($('#attachmentmenu'), button, e);
})
- .append($('<span>').attr('class', 'inner').text(label))
- .appendTo(item);
+ .append($('<span>').attr('class', 'inner').text(label));
+
+ if (fname.length) {
+ button.insertAfter(fname);
+ }
+ else {
+ button.appendTo(item);
+ }
}
};
/**
* Mailto menu
*/
function mailtomenu(obj, button, event, onclick)
{
var mailto = $(button).attr('href').replace(/^mailto:/, '');
if (mailto.indexOf('@') < 0) {
return true; // let the browser handle this
}
// disable all menu actions
obj.find('a').off('click').removeClass('active');
if (rcmail.env.has_writeable_addressbook) {
$('.addressbook', obj).addClass('active')
.on('click', function(e) {
var i, contact = mailto,
txt = $(button).filter('.rcmContactAddress').text();
contact = contact.split('?')[0].split(',')[0].replace(/(^<|>$)/g, '');
if (txt) {
txt = txt.replace('<' + contact + '>', '');
contact = '"' + $.trim(txt) + '" <' + contact + '>';
}
return rcmail.command('add-contact', contact, this, e.originalEvent);
});
}
$('.compose', obj).addClass('active').on('click', function(e) {
// Execute the original onclick handler to support mailto URL arguments (#6751)
if (onclick) {
button.onclick = onclick;
// use the second argument to tell our handler to not display the menu again
$(button).trigger('click', [true]);
button.onclick = null;
}
else {
rcmail.command('compose', mailto, this, e.originalEvent);
}
return false; // for Chrome
});
return rcmail.command('menu-open', {menu: 'mailto-menu', link: button}, button, event.originalEvent);
};
/**
* Appends popup menu to mailto links
*/
function mailtomenu_append(item)
{
// Remember the original onclick handler and display the menu instead
var onclick = item.onclick;
item.onclick = null;
$(item).on('click', function(e, menu) {
return menu || mailtomenu($('#mailto-menu'), item, e, onclick);
});
};
/**
* Headers menu in mail compose
*/
function headersmenu(obj, button, event)
{
$('li > a', obj).each(function() {
var link = $(this), target = '#compose_' + link.data('target');
link[$(target).is(':visible') ? 'removeClass' : 'addClass']('active')
.off().on('click', function() {
$(target).removeClass('hidden').find('.recipient-input input').focus();
link.removeClass('active');
rcmail.set_menu_buttons();
});
});
};
/**
* Reset/hide compose message recipient input
*/
function header_reset(id)
{
$('#' + id).val('').change()
// jump to the next input
.closest('.form-group').nextAll(':not(.hidden)').first().find('input').focus();
$('a[data-target=' + id.replace(/^_/, '') + ']').addClass('active');
rcmail.set_menu_buttons();
};
/**
* Recipient (contact) selector
*/
function recipient_selector(field, opts)
{
if (!opts) opts = {};
var title = rcmail.gettext(opts.title || 'insertcontact'),
dialog = $('#recipient-dialog'),
parent = dialog.parent(),
close_func = function() {
if (dialog.is(':visible')) {
rcmail.env.recipient_dialog.dialog('close');
}
},
insert_func = function() {
if (opts.action) {
opts.action();
close_func();
return;
}
rcmail.command('add-recipient');
};
if (!rcmail.env.recipient_selector_initialized) {
rcmail.addEventListener('add-recipient', close_func);
rcmail.env.recipient_selector_initialized = true;
}
if (field) {
rcmail.env.focused_field = '#_' + field;
}
rcmail.contact_list.clear_selection();
rcmail.contact_list.multiselect = 'multiselect' in opts ? opts.multiselect : true;
rcmail.env.recipient_dialog = rcmail.simple_dialog(dialog, title, insert_func, {
button: rcmail.gettext(opts.button || 'insert'),
button_class: opts.button_class || 'insert recipient',
height: 600,
classes: {
'ui-dialog-content': 'p-0' // remove padding on dialog content
},
open: function() {
// Don't want focus in the search field, we focus first contacts source record instead
$('#directorylist a').first().focus();
},
close: function() {
dialog.appendTo(parent);
$(this).remove();
$(opts.focus || rcmail.env.focused_field).focus();
}
});
};
/**
* Create/Update quota widget (setquota event handler)
*/
function update_quota(p)
{
var element = $('#quotadisplay'),
bar = element.find('.bar'),
value = p.total ? p.percent : 0;
if (!bar.length) {
bar = $('<span class="bar"><span class="value"></span></span>').appendTo(element);
}
if (value > 0 && value < 10) {
value = 10; // smaller values look not so nice
}
bar.find('.value').css('width', value + '%')[value >= 90 ? 'addClass' : 'removeClass']('warning');
// set title and reset tooltip's data (needed in case of empty title)
element.attr({'data-original-title': '', title: element.find('.count').attr('title')});
if (p.table) {
element.css('cursor', 'pointer').data('popup-pos', 'top')
.off('click').on('click', function(e) {
rcmail.simple_dialog(p.table, 'quota', null, {cancel_button: 'close'});
});
}
else {
element.tooltip('dispose').tooltip({trigger: is_mobile() ? 'click' : 'hover'});
}
};
/**
* Replaces recipient input with content-editable element that uses "recipient boxes"
*/
function recipient_input(obj)
{
var list, input, ac_props,
input_len_update = function() {
input.css('width', Math.max(40, input.val().length * 15 + 25));
},
apply_func = function() {
// update the original input
$(obj).val(list.text() + input.val());
},
insert_recipient = function(name, email, replace) {
var recipient = $('<li class="recipient">'),
name_element = $('<span class="name">').html(recipient_input_name(name || email))
.on('dblclick', function(e) { recipient_input_edit_dialog(e, insert_recipient); }),
email_element = $('<span class="email">'),
// TODO: should the 'close' link have tabindex?
link = $('<a>').attr({'class': 'button icon remove'})
.click(function() {
recipient.remove();
apply_func();
input.focus();
return false;
});
if (name) {
email = ' <' + email + '>';
}
email_element.text((name ? email : '') + ',');
recipient.attr('title', name ? (name + email) : null)
.append([name_element, email_element, link])
if (replace)
replace.replaceWith(recipient);
else
recipient.insertBefore(input.parent());
},
update_func = function(text) {
var result;
text = (text || input.val()).replace(/[,;\s]+$/, '');
result = recipient_input_parser(text);
$.each(result.recipients, function() {
insert_recipient(this.name, this.email);
});
// setTimeout() here is needed for proper input reset on paste event
// This is also the reason why we need parse_lock
setTimeout(function() {
input.val(result.text);
apply_func();
input_len_update();
}, 1);
return result.recipients.length > 0;
},
parse_func = function(e) {
// On paste the text is not yet in the input we have to use clipboard.
// Also because on paste new-line characters are replaced by spaces (#6460)
update_func(e.type == 'paste' ? (e.originalEvent.clipboardData || window.clipboardData).getData('text') : this.value);
},
keydown_func = function(e) {
// On Backspace remove the last recipient
if (e.keyCode == 8 && !input.val().length) {
list.children('li.recipient').last().remove();
apply_func();
return false;
}
// Here we add a recipient box when the separator (,;) or Enter was pressed
else if (e.key == ',' || e.key == ';' || (e.key == 'Enter' && !rcmail.ksearch_visible())) {
if (update_func()) {
return false;
}
}
input_len_update();
};
// Create the input element and "editable" area
input = $('<input>').attr({type: 'text', tabindex: $(obj).attr('tabindex')})
.on('paste change', parse_func)
.on('input', input_len_update) // only to fix input length after paste
.on('keydown', keydown_func)
.on('blur', function() { list.removeClass('focus'); })
.on('focus mousedown', function() { list.addClass('focus'); });
list = $('<ul>').addClass('form-control recipient-input')
.append($('<li>').append(input))
.on('click', function() { input.focus(); });
// Hide the original input/textarea
// Note: we do not remove the original element, and we do not use
// display: none, because we want to handle onfocus event
// Note: tabindex:-1 to make Shift+TAB working on these widgets
$(obj).css({position: 'absolute', opacity: 0, left: '-5000px', width: '10px'})
.attr('tabindex', -1)
.after(list)
// some core code sometimes focuses or changes the original node
// in such cases we wan't to parse it's value and apply changes
// to the widget element
.on('focus', function(e) { input.focus(); })
.on('change', function(e) {
$('li.recipient', list).remove();
input.val(this.value).change();
})
// copy and parse the value already set
.change();
// this one line is here to fix border of Bootstrap's input-group,
// input-group should not contain any hidden elements
$(obj).detach().insertBefore(list.parent());
if (rcmail.env.autocomplete_threads > 0) {
ac_props = {
threads: rcmail.env.autocomplete_threads,
sources: rcmail.env.autocomplete_sources
};
}
// Init autocompletion
rcmail.init_address_input_events(input, ac_props);
};
/**
* Parses recipient address input and extracts recipients from it
*/
function recipient_input_parser(text)
{
// support new-line as a separator, for paste action (#6460)
text = $.trim(text.replace(/[,;\s]*[\r\n]+/g, ','));
var recipients = [],
address_rx_part = '(\\S+|("[^"]+"))@\\S+',
recipient_rx1 = new RegExp('(<' + address_rx_part + '>)'),
recipient_rx2 = new RegExp('(' + address_rx_part + ')'),
global_rx = /(?=\S)[^",;]*(?:"[^\\"]*(?:\\[,;\S][^\\"]*)*"[^",;]*)*/g,
matches = text.match(global_rx);
$.each(matches || [], function() {
if (this.length && (recipient_rx1.test(this) || recipient_rx2.test(this))) {
var email = RegExp.$1,
name = $.trim(this.replace(email, ''));
recipients.push({
name: name,
email: email.replace(/(^<|>$)/g, ''),
text: this
});
text = text.replace(this, '');
}
});
text = text.replace(/[,;]+/, ',').replace(/^[,;\s]+/, '');
return {recipients: recipients, text: text};
};
/**
* Generates HTML for a text adding <span class="hidden">
* for quote/backslash characters, so they are hidden from the user,
* but still in place to make copying simpler
*
* Note: Selection works in Chrome, but not in Firefox?
*/
function recipient_input_name(text)
{
var i, char, result = '', len = text.length;
if (text.charAt(0) != '"' && text.indexOf('"') > -1) {
text = '"' + text.replace('\\', '\\\\').replace('"', '\\"') + '"';
}
for (i=0; i<len; i++) {
char = text.charAt(i);
switch (char) {
case '"':
if (i > 0 && i < len - 1) {
result += '"';
break;
}
result += '<span class="quotes">' + char + '</span>';
break;
case '\\':
result += '<span class="quotes">' + char + '</span>';
if (text.charAt(i+1) == '\\') {
result += char;
i++;
}
break;
case '<':
result += '<';
break;
case '>':
result += '>';
break;
default:
result += char;
}
}
return result;
};
/**
* Displays dialog to edit a recipient entry
*/
function recipient_input_edit_dialog(e, callback)
{
var element = $(e.target).parents('.recipient'),
recipient = element.text().replace(/,+$/, ''),
input = $('<input>').attr({type: 'text', size: 50}).val(recipient),
content = $('<label>').text(rcmail.gettext('recipient')).append(input);
rcmail.simple_dialog(content, 'recipientedit', function() {
var result, value = input.val();
if (value) {
if (value != recipient) {
result = recipient_input_parser(value);
if (result.recipients.length != 1) {
return false;
}
callback(result.recipients[0].name, result.recipients[0].email, element);
}
return true;
}
});
};
/**
* Adds logic to the contact photo widget
*/
function image_upload_input(obj)
{
var reset_button = $('<a>')
.attr({'class': 'icon button delete', href: '#', })
.click(function(e) { rcmail.command('delete-photo', '', this, e); return false; }),
img = $(obj).find('img')[0],
img_onload = function() {
var state = (img.currentSrc || img.src).indexOf(rcmail.env.photo_placeholder) != -1;
$(obj)[state ? 'removeClass' : 'addClass']('changed');
};
$(obj).append(reset_button).click(function() { rcmail.upload_input('upload-form'); });
// Note: Looks like only Firefox does not need this separate call
img_onload();
$(img).on('load', img_onload);
};
/**
* Displays loading... overlay for iframes
*/
function iframe_loader(frame)
{
frame = $(frame);
if (frame.length) {
var loader = $('<div class="iframe-loader">')
.append($('<div class="spinner spinner-border" role="status">')
.append($('<span class="sr-only">').text(rcmail.gettext('loading'))));
// custom 'loaded' event is expected to be triggered by plugins
// when using the loader not on an iframe
frame.on('load error loaded', function() {
// wait some time to make sure the iframe stopped loading
setTimeout(function() { loader.remove(); }, 500);
})
.parent().append(loader);
// fix scrolling in iOS
if (ios) {
frame.parent().addClass('ios-scroll');
}
}
};
/**
* Convert checkbox input into Bootstrap's custom switch
*/
function pretty_checkbox(checkbox)
{
var label, parent, id, checkbox = $(checkbox);
if (checkbox.is('.custom-control-input')) {
return;
}
if (!(id = checkbox.attr('id'))) {
id = 'icochk' + (++env.checkboxes);
checkbox.attr('id', id);
}
if (checkbox.parent().is('label')) {
label = checkbox.parent();
checkbox = checkbox.detach();
label.before(checkbox);
}
else {
label = $('<label>');
}
label.attr({'for': id, 'class': 'custom-control-label', title: checkbox.attr('title') || ''})
.on('click', function(e) { e.stopPropagation(); });
checkbox.addClass('form-check-input custom-control-input')
.wrap('<div class="custom-control custom-switch">')
.parent().append(label);
};
/**
* Make select dropdowns pretty
* TODO: searching, optgroup, [multiple], iPhone/iPad
*/
function pretty_select(select)
{
// iPhone is not supported yet (problem with browser dropdown on focus)
if (bw.iphone || bw.ipad) {
return;
}
select = $(select);
if (select.is('.pretty-select')) {
return;
}
var select_ident = 'select' + select.attr('id') + select.attr('name');
var is_menu_open = function() {
// Use proper window in cases when the select element intialized
// inside an iframe is then used in a dialog inside a parent's window
// For some reason we can't access data-button property in cross-window
// case, we use data-ident attribute instead
var win = select[0].ownerDocument.defaultView;
if (win.$('.select-menu .listing').data('ident') == select_ident) {
return true;
}
};
var close_func = function() {
var open = is_menu_open();
select.popover('dispose').focus();
return !open;
};
var open_func = function(e) {
var items = [],
dialog = select.closest('.ui-dialog')[0],
max_height = (document.documentElement.clientHeight || $(document.body).height()) - 75,
max_width = $(document.body).width() - 20,
min_width = Math.min(select.outerWidth(), max_width),
value = select.val();
if (!is_mobile()) {
max_height *= 0.5;
}
// close other popups
popups_close(e);
$('option', select).each(function() {
var label = $(this).text(),
link = $('<a href="#">')
.data('value', this.value)
.addClass(this.disabled ? 'disabled' : 'active' + (this.value == value ? ' selected' : ''));
if (label.length) {
link.text(label);
}
else {
link.html(' '); // link can't be empty
}
items.push($('<li>').append(link));
});
var list = $('<ul class="listing selectable iconized">')
.attr('data-ident', select_ident)
.data('button', select[0])
.append(items)
.on('click', 'a.active', function() {
// first close the list, then update the select, the order is important
// for cases when the select might be removed in change event (datepicker)
var val = $(this).data('value'), ret = close_func();
select.val(val).change();
return ret;
})
.on('keydown', 'a.active', function(e) {
var item, node, mode = 'next';
switch (e.which) {
case 27: // ESC
case 9: // TAB
return close_func();
case 13: // ENTER
case 32: // SPACE
$(this).click();
return false; // for IE
case 38: // ARROW-UP
case 63232:
mode = 'previous';
case 40: // ARROW-DOWN
case 63233:
item = e.target.parentNode;
while (item = item[mode + 'Sibling']) {
if (node = $(item).children('.active')[0]) {
node.focus();
break;
}
}
return false; // prevents from scrolling the whole page
}
});
select.popover('dispose')
.popover({
// because of focus issues we can't always use body,
// if select is in a dialog, popover has to be a child of this dialog
container: dialog || document.body,
content: list[0],
placement: 'bottom',
trigger: 'manual',
boundary: 'viewport',
html: true,
offset: '0,2',
sanitize: false,
template: '<div class="popover select-menu" style="min-width: ' + min_width + 'px; max-width: ' + max_width + 'px">'
+ '<div class="popover-header"></div>'
+ '<div class="popover-body" style="max-height: ' + max_height + 'px"></div></div>'
})
.on('shown.bs.popover', function() {
select.focus(); // for Chrome
// Set popup Close title
list.parent().prev()
.empty()
.append($('<a class="button icon cancel">').text(rcmail.gettext('close'))
.on('click', function(e) {
e.stopPropagation();
return close_func();
})
);
// focus first active element on the list
if (rcube_event.is_keyboard(e)) {
list.find('a.active').first().focus();
}
// don't propagate mousedown event
list.on('mousedown', function(e) { e.stopPropagation(); });
})
.popover('show');
};
select.addClass('pretty-select custom-select form-control')
.on('mousedown keydown', function(e) {
select = $(e.target); // so it works after clone
// Do nothing on disabled select or on TAB key
if (select.prop('disabled')) {
return;
}
if (e.which == 9) {
close_func();
return true;
}
// Close popup on ESC key or on click if already open
if (e.which == 27 || (e.type == 'mousedown' && is_menu_open())) {
return close_func();
}
select.focus();
// prevent displaying browser-default select dropdown
select.prop('disabled', true);
setTimeout(function() { select.prop('disabled', false); }, 0);
e.stopPropagation();
// display options in our way (on SPACE, ENTER, ARROW-DOWN or mousedown)
if (e.type == 'mousedown' || e.which == 13 || e.which == 32 || e.which == 40 || e.which == 63233) {
open_func(e);
// Prevent from closing the menu by general popover closing handler (popups_close())
// We used to just stop propagation in onclick handler, but it didn't work
// in Chrome where onclick handler wasn't invoked on mobile (#6705)
popups_close_lock = new Date().getTime();
return false;
}
})
};
/**
* HTML editor textarea wrapper with nice looking tabs-like switch
*/
function html_editor_init(obj)
{
// Here we support two structures
// 1. <div><textarea></textarea><select name="editorSelector"></div>
// 2. <tr><td><td><td><textarea></textarea></td></tr>
// <tr><td><td><td><input type="checkbox"></td></tr>
var sw, is_table = false,
editor = $(obj),
parent = editor.parent(),
tabindex = editor.attr('tabindex'),
mode = function() {
if (is_table) {
return sw.is(':checked') ? 'html' : 'plain';
}
return sw.val();
},
tabs = $('<ul class="nav nav-tabs">')
.append($('<li class="nav-item">')
.append($('<a class="nav-link mode-html" href="#">')
.text(rcmail.gettext('htmltoggle'))))
.append($('<li class="nav-item">')
.append($('<a class="nav-link mode-plain" href="#">')
.text(rcmail.gettext('plaintoggle'))));
if (parent.is('td')) {
sw = $('input[type="checkbox"]', parent.parent().next());
is_table = true;
}
else {
sw = $('[name="editorSelector"]', obj.form);
}
// sanity check
if (sw.length != 1) {
return;
}
parent.addClass('html-editor');
editor.before(tabs);
$('a', tabs).attr('tabindex', tabindex)
.on('click', function(e) {
var id = editor.attr('id'), is_html = $(this).is('.mode-html');
e.preventDefault();
if (rcmail.command('toggle-editor', {id: id, html: is_html}, '', e.originalEvent)) {
$(this).tab('show').prop('tabindex', -1);
$('.mode-' + (is_html ? 'plain' : 'html'), tabs).prop('tabindex', tabindex);
if (is_table) {
sw.prop('checked', is_html);
}
}
})
.filter('.mode-' + mode()).tab('show').prop('tabindex', -1);
if (is_table) {
// Hide unwanted table cells
sw.parents('tr').first().hide();
parent.prev().hide();
// Modify the textarea cell to use 100% width
parent.addClass('col-sm-12');
}
// make the textarea autoresizeable
textarea_autoresize_init(editor);
};
/**
* Make the textarea autoresizeable depending on it's content length.
* The way there's no vertical scrollbar.
*/
function textarea_autoresize_init(textarea)
{
var resize = function(e) {
clearTimeout(env.textarea_timer);
env.textarea_timer = setTimeout(function() {
var area = $(e.target),
initial_height = area.data('initial-height'),
scroll_height = area[0].scrollHeight;
// do nothing when the area is hidden
if (!scroll_height) {
return;
}
if (!initial_height) {
area.data('initial-height', initial_height = scroll_height);
}
// strange effect in Chrome/Firefox when you delete a line in the textarea
// the scrollHeight is not decreased by the line height, but by 2px
// so jumps up many times in small steps, we'd rather use one big step
if (area.outerHeight() - scroll_height == 2) {
scroll_height -= 19; // 21px is the assumed line height
}
area.outerHeight(Math.max(initial_height, scroll_height));
}, 10);
};
$(textarea).css('overflow-y', 'hidden').on('input', resize).trigger('input');
// Make sure the height is up-to-date also in time intervals
setInterval(function() { $(textarea).trigger('input'); }, 1000);
};
// Inititalizes smart list input
function smart_field_init(field)
{
var tip, id = field.id + '_list',
area = $('<div class="multi-input"><div class="content"></div><div class="invalid-feedback"></div></div>'),
list = field.value ? field.value.split("\n") : [''];
if ($('#' + id).length) {
return;
}
// add input rows
$.each(list, function(i, v) {
smart_field_row_add($('.content', area), v, field.name, i, $(field).data('size'));
});
area.attr('id', id);
field = $(field);
if (field.attr('disabled')) {
area.hide();
}
// disable the original field anyway, we don't want it in POST
else {
field.prop('disabled', true);
}
if (field.data('hidden')) {
area.hide();
}
field.after(area);
if (field.hasClass('is-invalid')) {
area.addClass('is-invalid');
$('.invalid-feedback', area).text(field.data('error-msg'));
}
};
function smart_field_row_add(area, value, name, idx, size, after)
{
// build row element content
var input, elem = $('<div class="input-group">'
+ '<input type="text" class="form-control">'
+ '<span class="input-group-append"><a class="icon reset input-group-text" href="#"></a></span>'
+ '</div>'),
attrs = {value: value, name: name + '[]'};
if (size) {
attrs.size = size;
}
input = $('input', elem).attr(attrs)
.keydown(function(e) {
var input = $(this);
// element creation event (on Enter)
if (e.which == 13) {
var name = input.attr('name').replace(/\[\]$/, ''),
dt = (new Date()).getTime(),
elem = smart_field_row_add(area, '', name, dt, size, input.parent());
$('input', elem).focus();
}
// backspace or delete: remove input, focus previous one
else if ((e.which == 8 || e.which == 46) && input.val() == '') {
var parent = input.parent(),
siblings = area.children();
if (siblings.length > 1) {
if (parent.prev().length) {
parent.prev().children('input').focus();
}
else {
parent.next().children('input').focus();
}
parent.remove();
return false;
}
}
});
// element deletion event
$('a.reset', elem).click(function() {
var record = $(this.parentNode.parentNode);
if (area.children().length > 1) {
$('input', record.next().length ? record.next() : record.prev()).focus();
record.remove();
}
else {
$('input', record).val('').focus();
}
});
$(elem).find('input,a')
.on('focus', function() { area.addClass('focused'); })
.on('blur', function() { area.removeClass('focused'); });
if (after) {
after.after(elem);
}
else {
elem.appendTo(area);
}
return elem;
};
// Reset and fill the smart list input with new data
function smart_field_reset(field, data)
{
var id = field.id + '_list',
list = data.length ? data : [''],
area = $('#' + id).children('.content');
area.empty();
// add input rows
$.each(list, function(i, v) {
smart_field_row_add(area, v, field.name, i, $(field).data('size'));
});
};
/**
* Register form errors, mark fields as invalid, dsplay the error below the input
*/
function form_errors(tips)
{
$.each(tips, function() {
var input = $('#' + this[0]).addClass('is-invalid');
if (input.data('type') == 'list') {
input.data('error-msg', this[2]);
$('#' + this[0] + '_list > .invalid-feedback').text(this[2]);
return;
}
input.after($('<span class="invalid-feedback">').text(this[2]));
});
};
/**
* Show/hide the navigation list
*/
function switch_nav_list(obj)
{
var records, height, speed = 250,
button = $('a', obj),
navlist = $(obj).next();
if (!navlist.height()) {
records = $('tr,li', navlist).filter(function() { return this.style.display != 'none'; });
height = $(records[0]).height() || 50;
navlist.animate({height: (Math.min(5, records.length) * height + 1) + 'px'}, speed);
button.addClass('collapse').removeClass('expand');
$(obj).addClass('expanded');
}
else {
navlist.animate({height: '0'}, speed);
button.addClass('expand').removeClass('collapse');
$(obj).removeClass('expanded');
}
};
/**
* Wrapper for rcmail.open_window to intercept window opening
* and display a dialog with an iframe instead of a real window.
*/
function window_open(url)
{
// Use 4th argument to bypass the dialog-mode e.g. for external windows
if (!is_mobile() || arguments[3] === true) {
return env.open_window.apply(rcmail, arguments);
}
// _extwin=1, _framed=1 are required to display attachment preview
// layout properly and make mobile menus working
url = rcmail.add_url(url, '_framed', 1);
url = rcmail.add_url(url, '_extwin', 1);
var label, title = '',
props = {cancel_button: 'close', width: 768, height: 768},
frame = $('<iframe>').attr({id: 'windowframe', src: url});
if (/_action=([a-z_]+)/.test(url) && (label = rcmail.labels[RegExp.$1])) {
title = label;
}
if (/_frame=1/.test(url)) {
props.dialogClass = 'no-titlebar';
}
rcmail.simple_dialog(frame, title, null, props);
return true;
};
/**
* Get layout modes. In frame mode returns the parent layout modes.
*/
function layout_metadata()
{
if (is_framed) {
var doc = $(parent.document.documentElement);
return {
mode: doc[0].className.match(/layout-([a-z]+)/) ? RegExp.$1 : mode,
touch: doc.is('.touch'),
};
}
return {mode: mode, touch: touch};
};
/**
* Returns true if the layout is in 'small' or 'phone' mode
*/
function is_mobile()
{
var meta = layout_metadata();
return meta.mode == 'phone' || meta.mode == 'small';
};
/**
* Returns true if the layout is in 'touch' mode
*/
function is_touch()
{
var meta = layout_metadata();
return meta.touch;
};
}
if (window.rcmail) {
/**
* Elastic version of show_menu as we don't need e.g. menu positioning from core
* TODO: keyboard navigation in menus
*/
rcmail.show_menu = function(prop, show, event)
{
var name = typeof prop == 'object' ? prop.menu : prop,
obj = $('#' + name);
if (typeof prop == 'string') {
prop = {menu: name};
}
// just delegate the action to rcube_elastic_ui
return rcmail.triggerEvent(show === false ? 'menu-close' : 'menu-open', {name: name, obj: obj, props: prop, originalEvent: event});
}
/**
* Elastic version of hide_menu as we don't need e.g. menus stack handling
*/
rcmail.hide_menu = function(name, event)
{
// delegate to rcube_elastic_ui
return rcmail.triggerEvent('menu-close', {name: name, props: {menu: name}, originalEvent: event});
}
}
else {
// rcmail does not exists e.g. on the error template inside a frame
// we fake the engine a little
var rcmail = parent.rcmail,
rcube_webmail = parent.rcube_webmail,
bw = {};
}
var UI = new rcube_elastic_ui();
// Improve non-inline datepickers
if ($ && $.datepicker) {
var __newInst = $.datepicker._newInst;
$.extend($.datepicker, {
_newInst: function(target, inline) {
var inst = __newInst.call(this, target, inline);
if (!inst.inline) {
UI.datepicker_init(inst.dpDiv);
}
return inst;
}
});
}
diff --git a/skins/larry/ui.js b/skins/larry/ui.js
index 270aeb051..182a13673 100644
--- a/skins/larry/ui.js
+++ b/skins/larry/ui.js
@@ -1,1515 +1,1524 @@
/**
* Roundcube functions for default skin interface
*
* Copyright (c) The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*
* @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
*/
function rcube_mail_ui()
{
var env = {};
var popups = {};
var popupconfig = {
forwardmenu: { editable:1 },
searchmenu: { editable:1, callback:searchmenu },
attachmentmenu: { },
listoptions: { editable:1 },
groupmenu: { above:1 },
mailboxmenu: { above:1 },
spellmenu: { callback: spellmenu },
'folder-selector': { iconized:1 }
};
var me = this;
var mailviewsplit;
var mailviewsplit2;
var compose_headers = {};
var prefs;
// export public methods
this.set = setenv;
this.init = init;
this.init_tabs = init_tabs;
this.show_about = show_about;
this.show_popup = show_popup;
this.toggle_popup = toggle_popup;
this.add_popup = add_popup;
this.import_dialog = import_dialog;
this.set_searchmod = set_searchmod;
this.set_searchscope = set_searchscope;
this.show_header_row = show_header_row;
this.hide_header_row = hide_header_row;
this.update_quota = update_quota;
this.get_pref = get_pref;
this.save_pref = save_pref;
this.folder_search_init = folder_search_init;
// set minimal mode on small screens (don't wait for document.ready)
if (window.$ && document.body) {
var minmode = get_pref('minimalmode');
if (parseInt(minmode) || (minmode === null && $(window).height() < 850)) {
$(document.body).addClass('minimal');
}
if (bw.tablet) {
$('#viewport').attr('content', "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0");
}
$(document).ready(function() { me.init(); });
}
/**
*
*/
function setenv(key, val)
{
env[key] = val;
}
/**
* Get preference stored in browser
*/
function get_pref(key)
{
if (!prefs) {
prefs = rcmail.local_storage_get_item('prefs.larry', {});
}
// fall-back to cookies
if (prefs[key] == null) {
var cookie = rcmail.get_cookie(key);
if (cookie != null) {
prefs[key] = cookie;
// copy value to local storage and remove cookie (if localStorage is supported)
if (rcmail.local_storage_set_item('prefs.larry', prefs)) {
rcmail.set_cookie(key, cookie, new Date()); // expire cookie
}
}
}
return prefs[key];
}
/**
* Saves preference value to browser storage
*/
function save_pref(key, val)
{
prefs[key] = val;
// write prefs to local storage (if supported)
if (!rcmail.local_storage_set_item('prefs.larry', prefs)) {
// store value in cookie
var exp = new Date();
exp.setYear(exp.getFullYear() + 1);
rcmail.set_cookie(key, val, exp);
}
}
/**
* Initialize UI
* Called on document.ready
*/
function init()
{
rcmail.addEventListener('message', message_displayed);
$.widget('ui.dialog', $.ui.dialog, {
open: function() {
this._super();
dialog_open(this);
return this;
}});
/*** prepare minmode functions ***/
$('#taskbar a').each(function(i,elem){
$(elem).append('<span class="tooltip">' + $('.button-inner', this).html() + '</span>')
});
$('#taskbar .minmodetoggle').click(function(e){
var ismin = $(document.body).toggleClass('minimal').hasClass('minimal');
save_pref('minimalmode', ismin?1:0);
$(window).resize();
});
/*** mail task ***/
if (rcmail.env.task == 'mail') {
rcmail.addEventListener('menu-open', menu_toggle)
.addEventListener('menu-close', menu_toggle)
.addEventListener('menu-save', save_listoptions)
.addEventListener('enable-command', enable_command)
.addEventListener('responseafterlist', function(e){ switch_view_mode(rcmail.env.threading ? 'thread' : 'list', true) })
.addEventListener('responseaftersearch', function(e){ switch_view_mode(rcmail.env.threading ? 'thread' : 'list', true) });
var dragmenu = $('#dragmessagemenu');
if (dragmenu.length) {
rcmail.gui_object('dragmenu', 'dragmessagemenu');
popups.dragmenu = dragmenu;
}
if (rcmail.env.action == 'show' || rcmail.env.action == 'preview') {
rcmail.addEventListener('aftershow-headers', function() { layout_messageview(); })
.addEventListener('afterhide-headers', function() { layout_messageview(); });
$('#previewheaderstoggle').click(function(e) {
toggle_preview_headers();
if (this.blur && !rcube_event.is_keyboard(e))
this.blur();
return false;
});
// add menu link for each attachment
$('#attachment-list > li').each(function() {
attachmentmenu_append(this);
});
if (get_pref('previewheaders') == '1') {
toggle_preview_headers();
}
if (rcmail.env.action == 'show') {
$('#messagecontent').focus();
}
}
else if (rcmail.env.action == 'compose') {
rcmail.addEventListener('fileappended', function(e) { if (e.attachment.complete) attachmentmenu_append(e.item); })
.addEventListener('aftertoggle-editor', function(e) {
window.setTimeout(function() { layout_composeview() }, 200);
if (e && e.mode)
$("select[name='editorSelector']").val(e.mode);
})
.addEventListener('compose-encrypted', function(e) {
$("select[name='editorSelector']").prop('disabled', e.active);
$('a.button.attach, a.button.responses')[(e.active?'addClass':'removeClass')]('disabled');
$('#responseslist a.insertresponse')[(e.active?'removeClass':'addClass')]('active');
});
init_compose_editfields();
$('#composeoptionstoggle').click(function(e){
var expanded = $('#composeoptions').toggle().is(':visible');
$('#composeoptionstoggle').toggleClass('remove').attr('aria-expanded', expanded ? 'true' : 'false');
layout_composeview();
save_pref('composeoptions', expanded ? '1' : '0');
if (!rcube_event.is_keyboard(e))
this.blur();
return false;
}).css('cursor', 'pointer');
if (get_pref('composeoptions') !== '0') {
$('#composeoptionstoggle').click();
}
// toggle compose options if opened in new window and they were visible before
var opener_rc = rcmail.opener();
if (opener_rc && opener_rc.env.action == 'compose' && $('#composeoptionstoggle', opener.document).hasClass('remove'))
$('#composeoptionstoggle').click();
new rcube_splitter({ id:'composesplitterv', p1:'#composeview-left', p2:'#composeview-right',
orientation:'v', relative:true, start:206, min:170, size:12, render:layout_composeview }).init();
// add menu link for each attachment
$('#attachment-list > li').each(function() {
attachmentmenu_append(this);
});
}
else if (rcmail.env.action == 'bounce') {
init_compose_editfields();
}
else if (rcmail.env.action == 'list' || !rcmail.env.action) {
mail_layout();
$('#maillistmode').addClass(rcmail.env.threading ? '' : 'selected').click(function(e) { switch_view_mode('list'); return false; });
$('#mailthreadmode').addClass(rcmail.env.threading ? 'selected' : '').click(function(e) { switch_view_mode('thread'); return false; });
rcmail.init_pagejumper('#pagejumper');
rcmail.addEventListener('setquota', update_quota)
.addEventListener('layout-change', mail_layout);
}
else if (rcmail.env.action == 'get') {
new rcube_splitter({ id:'mailpartsplitterv', p1:'#messagepartheader', p2:'#messagepartcontainer',
orientation:'v', relative:true, start:226, min:150, size:12}).init();
}
if ($('#mailview-left').length) {
new rcube_splitter({ id:'mailviewsplitterv', p1:'#mailview-left', p2:'#mailview-right',
orientation:'v', relative:true, start:206, min:150, size:12, callback:render_mailboxlist, render:resize_leftcol }).init();
}
}
/*** settings task ***/
else if (rcmail.env.task == 'settings') {
rcmail.addEventListener('init', function(){
var tab = '#settingstabpreferences';
if (rcmail.env.action)
tab = '#settingstab' + (rcmail.env.action.indexOf('identity')>0 ? 'identities' : rcmail.env.action.replace(/\./g, ''));
$(tab).addClass('selected')
.children().first().removeAttr('onclick').click(function() { return false; });
});
if (rcmail.env.action == 'folders') {
new rcube_splitter({ id:'folderviewsplitter', p1:'#folderslist', p2:'#folder-details',
orientation:'v', relative:true, start:266, min:180, size:12 }).init();
rcmail.addEventListener('setquota', update_quota);
folder_search_init($('#folderslist'));
}
else if (rcmail.env.action == 'identities') {
new rcube_splitter({ id:'identviewsplitter', p1:'#identitieslist', p2:'#identity-details',
orientation:'v', relative:true, start:266, min:180, size:12 }).init();
}
else if (rcmail.env.action == 'responses') {
new rcube_splitter({ id:'responseviewsplitter', p1:'#responseslist', p2:'#response-details',
orientation:'v', relative:true, start:266, min:180, size:12 }).init();
}
else if (rcmail.env.action == 'preferences' || !rcmail.env.action) {
new rcube_splitter({ id:'prefviewsplitter', p1:'#sectionslist', p2:'#preferences-box',
orientation:'v', relative:true, start:266, min:180, size:12 }).init();
}
else if (rcmail.env.action == 'edit-prefs') {
var legend = $('#preferences-details fieldset.advanced legend'),
toggle = $('<a href="#toggle"></a>')
.text(rcmail.gettext('toggleadvancedoptions'))
.attr('title', rcmail.gettext('toggleadvancedoptions'))
.addClass('advanced-toggle');
legend.click(function(e) {
toggle.html($(this).hasClass('collapsed') ? '▲' : '▼');
$(this).toggleClass('collapsed')
.closest('fieldset').children('.propform').toggle()
}).append(toggle).addClass('collapsed')
// this magically fixes incorrect position of toggle link created above in Firefox 3.6
if (bw.mz)
legend.parents('form').css('display', 'inline');
}
}
/*** addressbook task ***/
else if (rcmail.env.task == 'addressbook') {
rcmail.addEventListener('beforepushgroup', push_contactgroup)
.addEventListener('beforepopgroup', pop_contactgroup)
.addEventListener('menu-open', menu_toggle)
.addEventListener('menu-close', menu_toggle);
if (rcmail.env.action == '') {
new rcube_splitter({ id:'addressviewsplitterd', p1:'#addressview-left', p2:'#addressview-right',
orientation:'v', relative:true, start:206, min:150, size:12, render:resize_leftcol }).init();
new rcube_splitter({ id:'addressviewsplitter', p1:'#addresslist', p2:'#contacts-box',
orientation:'v', relative:true, start:266, min:260, size:12 }).init();
}
var dragmenu = $('#dragcontactmenu');
if (dragmenu.length) {
rcmail.gui_object('dragmenu', 'dragcontactmenu');
popups.dragmenu = dragmenu;
}
}
// turn a group of fieldsets into tabs
$('.tabbed').each(function(idx, elem){ init_tabs(elem); })
// decorate select elements
$('select.decorated').each(function(){
if (bw.opera) {
$(this).removeClass('decorated');
return;
}
var select = $(this),
parent = select.parent(),
height = Math.max(select.height(), 26) - 2,
width = select.width() - 22,
title = $('option', this).first().text();
if ($('option:selected', this).val() != '')
title = $('option:selected', this).text();
var overlay = $('<a class="menuselector" tabindex="-1"><span class="handle">' + title + '</span></a>')
.css('position', 'absolute')
.offset(select.position())
.insertAfter(select);
overlay.children().width(width).height(height).css('line-height', (height - 1) + 'px');
if (parent.css('position') != 'absolute')
parent.css('position', 'relative');
// re-set original select width to fix click action and options width in some browsers
select.width(overlay.width())
.on(bw.mz ? 'change keyup' : 'change', function() {
var val = $('option:selected', this).text();
$(this).next().children().text(val);
});
select
.on('focus', function(e){ overlay.addClass('focus'); })
.on('blur', function(e){ overlay.removeClass('focus'); });
});
// set min-width to show all toolbar buttons
var screen = $('body.minwidth');
if (screen.length) {
screen.css('min-width', $('.toolbar').width() + $('#quicksearchbar').width() + $('#searchfilter').width() + 30);
}
// don't use $(window).resize() due to some unwanted side-effects
window.onresize = resize;
resize();
}
/**
* Update UI on window resize
*/
function resize(e)
{
// resize in intervals to prevent lags and double onresize calls in Chrome (#1489005)
var interval = e ? 10 : 0;
if (rcmail.resize_timeout)
window.clearTimeout(rcmail.resize_timeout);
rcmail.resize_timeout = window.setTimeout(function() {
if (rcmail.env.task == 'mail') {
if (rcmail.env.action == 'show' || rcmail.env.action == 'preview')
layout_messageview();
else if (rcmail.env.action == 'compose')
layout_composeview();
}
// make iframe footer buttons float if scrolling is active
$('body.iframe .footerleft').each(function(){
var footer = $(this),
body = $(document.body),
floating = footer.hasClass('floating'),
overflow = body.outerHeight(true) > $(window).height();
if (overflow != floating) {
var action = overflow ? 'addClass' : 'removeClass';
footer[action]('floating');
body[action]('floatingbuttons');
}
});
}, interval);
}
/**
* Triggered when a new user message is displayed
*/
function message_displayed(p)
{
var siblings = $(p.object).siblings('div');
if (siblings.length)
$(p.object).insertBefore(siblings.first());
// show a popup dialog on errors
if (p.type == 'error' && rcmail.env.task != 'login') {
// hide original message object, we don't want both
rcmail.hide_message(p.object);
if (me.message_timer) {
window.clearTimeout(me.message_timer);
}
if (!me.messagedialog) {
me.messagedialog = $('<div>').addClass('popupdialog').hide();
}
var msg = p.message,
dialog_close = function() {
// check if dialog is still displayed, to prevent from js error
me.messagedialog.is(':visible') && me.messagedialog.dialog('destroy').hide();
};
if (me.messagedialog.is(':visible') && me.messagedialog.text() != msg)
msg = me.messagedialog.html() + '<p>' + p.message + '</p>';
me.messagedialog.html(msg)
.dialog({
resizable: false,
closeOnEscape: true,
dialogClass: p.type,
title: rcmail.gettext('errortitle'),
close: dialog_close,
hide: {effect: 'fadeOut'},
width: 420,
minHeight: 90
}).show();
me.messagedialog.closest('div[role=dialog]').attr('role', 'alertdialog');
if (p.timeout > 0)
me.message_timer = window.setTimeout(dialog_close, p.timeout);
}
}
// modify dialog position to fully fit the close button into the window
function dialog_open(dialog)
{
var me = $(dialog.uiDialog),
offset = me.offset(),
position = me.position(),
width = me.outerWidth(),
maxWidth = $(window).width(),
topOffset = offset.top - 12;
if (topOffset < 0)
me.css('top', position.top - topOffset);
if (offset.left + width + 12 > maxWidth)
me.css('left', position.left - 12);
}
// Mail view layout initialization and change handler
function mail_layout(p)
{
var layout = p ? p.new_layout : rcmail.env.layout,
top = $('#mailview-top'),
bottom = $('#mailview-bottom');
if (p)
$('#mainscreencontent').removeClass().addClass(layout);
$('#mailviewsplitter')[layout == 'desktop' ? 'show' : 'hide']();
$('#mailviewsplitter2')[layout == 'widescreen' ? 'show' : 'hide']();
$('#mailpreviewframe')[layout != 'list' ? 'show' : 'hide']();
rcmail.env.contentframe = layout == 'list' ? null : 'messagecontframe';
if (layout == 'widescreen') {
$('#countcontrols').detach().appendTo($('#messagelistheader'));
top.css({height: 'auto', width: 394});
bottom.css({top: 0, left: 406, height: 'auto'}).show();
if (!mailviewsplit2) {
mailviewsplit2 = new rcube_splitter({ id:'mailviewsplitter2', p1:'#mailview-top', p2:'#mailview-bottom',
orientation:'v', relative:true, start:416, min:400, size:12});
mailviewsplit2.init();
}
else
mailviewsplit2.resize();
}
else if (layout == 'desktop') {
top.css({height: 270, width: 'auto'});
bottom.css({left: 0, top: 284, height: 'auto'}).show();
if (!mailviewsplit) {
mailviewsplit = new rcube_splitter({ id:'mailviewsplitter', p1:'#mailview-top', p2:'#mailview-bottom',
orientation:'h', relative:true, start:276, min:150, size:12, offset:4 });
mailviewsplit.init();
}
else
mailviewsplit.resize();
}
else { // layout == 'list'
top.css({height: 'auto', width: 'auto'});
bottom.hide();
}
if (p && p.old_layout == 'widescreen') {
$('#countcontrols').detach().appendTo($('#messagelistfooter'));
}
}
/**
* Adjust UI objects of the mail view screen
*/
function layout_messageview()
{
$('#messagecontent').css('top', ($('#messageheader').outerHeight() + 1) + 'px');
$('#message-objects div a').addClass('button');
if (!$('#attachment-list li').length) {
$('div.rightcol').hide().attr('aria-hidden', 'true');
$('div.leftcol').css('margin-right', '0');
}
var mvlpe = $('#messagebody.mailvelope, #messagebody > .mailvelope');
if (mvlpe.length) {
var h = $('#messagecontent').length ?
$('#messagecontent').height() - 16 :
$(window).height() - mvlpe.offset().top - 2;
mvlpe.height(h);
}
}
function render_mailboxlist(splitter)
{
// TODO: implement smart shortening of long folder names
}
function resize_leftcol(splitter)
{
// STUB
}
function init_compose_editfields()
{
// Show input elements with non-empty value
var f, v, field, fields = ['cc', 'bcc', 'replyto', 'followupto'];
for (f=0; f < fields.length; f++) {
v = fields[f]; field = $('#_'+v);
if (field.length) {
field.on('change', {v: v}, function(e) { if (this.value) show_header_row(e.data.v, true); });
if (field.val() != '')
show_header_row(v, true);
}
}
// adjust hight when textarea starts to scroll
$("textarea[name='_to'], textarea[name='_cc'], textarea[name='_bcc']").change(function(e){ adjust_compose_editfields(this); }).change();
rcmail.addEventListener('autocomplete_insert', function(p){ adjust_compose_editfields(p.field); });
}
function adjust_compose_editfields(elem)
{
if (elem.nodeName == 'TEXTAREA') {
var $elem = $(elem), line_height = 14, // hard-coded because some browsers only provide the outer height in elem.clientHeight
content_height = elem.scrollHeight,
rows = elem.value.length > 80 && content_height > line_height*1.5 ? 2 : 1;
$elem.css('height', (line_height*rows) + 'px');
layout_composeview();
}
}
function layout_composeview()
{
var body = $('#composebody'),
form = $('#compose-content'),
bottom = $('#composeview-bottom'),
w, h, bh, ovflw, btns = 0,
minheight = 300;
if (!form.length)
return;
bh = form.height() - bottom.position().top;
ovflw = minheight - bh;
btns = ovflw > -100 ? 0 : 40;
bottom.height(Math.max(minheight, bh));
form.css('overflow', ovflw > 0 ? 'auto' : 'hidden');
w = body.parent().width() - 5;
h = body.parent().height() - 8;
body.width(w).height(h);
$('#composebodycontainer > div').width(w+8);
$('#composebody_ifr').height(h + 4 - $('div.mce-toolbar').height());
$('#googie_edit_layer').width(w).height(h);
// $('#composebodycontainer')[(btns ? 'addClass' : 'removeClass')]('buttons');
// $('#composeformbuttons')[(btns ? 'show' : 'hide')]();
var abooks = $('#directorylist');
if (abooks.length)
$('#compose-contacts .scroller').css('top', abooks.position().top + abooks.outerHeight());
}
function update_quota(p)
{
var element = $('#quotadisplay'), menu = $('#quotamenu'),
step = 24, step_count = 20,
y = p.total ? Math.ceil(p.percent / 100 * step_count) * step : 0;
// never show full-circle if quota is close to 100% but below.
if (p.total && y == step * step_count && p.percent < 100)
y -= step;
element.css('background-position', '0 -' + y + 'px');
element.attr('class', 'countdisplay p' + (Math.round(p.percent / 10) * 10));
if (p.table) {
if (!menu.length)
menu = $('<div id="quotamenu" class="popupmenu">').appendTo($('body'));
menu.html(p.table);
element.css('cursor', 'pointer').off('click').on('click', function(e) {
return rcmail.command('menu-open', 'quotamenu', e.target, e);
});
}
}
function folder_search_init(container)
{
// animation to unfold list search box
$('.boxtitle a.search', container).click(function(e) {
var title = $('.boxtitle', container),
box = $('.listsearchbox', container),
dir = box.is(':visible') ? -1 : 1,
height = 34 + ($('select', box).length ? 22 : 0);
box.slideToggle({
duration: 160,
progress: function(animation, progress) {
if (dir < 0) progress = 1 - progress;
$('.scroller', container).css('top', (title.outerHeight() + height * progress) + 'px');
},
complete: function() {
box.toggleClass('expanded');
if (box.is(':visible')) {
box.find('input[type=text]').focus();
height = 34 + ($('select', box).length ? $('select', box).outerHeight() + 4 : 0);
$('.scroller', container).css('top', (title.outerHeight() + height) + 'px');
}
else {
$('a.reset', box).click();
}
// TODO: save state in localStorage
}
});
return false;
});
}
function enable_command(p)
{
if (p.command == 'reply-list' && rcmail.env.reply_all_mode == 1) {
var label = rcmail.gettext(p.status ? 'replylist' : 'replyall');
if (rcmail.env.action == 'preview')
$('a.button.replyall').attr('title', label);
else
$('a.button.reply-all').text(label).attr('title', label);
}
else if (p.command == 'compose-encrypted') {
// show the toolbar button for Mailvelope
$('a.button.encrypt').parent().show();
}
else if (p.command == 'compose-encrypted-signed') {
// enable selector for encrypt and sign
$('#encryptionmenulink').show();
}
}
/**
* Register a popup menu
*/
function add_popup(popup, config)
{
var obj = popups[popup] = $('#'+popup);
obj.appendTo(document.body); // move it to top for proper absolute positioning
if (obj.length)
popupconfig[popup] = $.extend(popupconfig[popup] || {}, config || {});
}
/**
* Trigger for popup menus
*/
function toggle_popup(popup, e, config)
{
// auto-register menu object
if (config || !popupconfig[popup])
add_popup(popup, config);
return rcmail.command('menu-open', popup, e.target, e);
}
/**
* (Deprecated) trigger for popup menus
*/
function show_popup(popup, show, config)
{
// auto-register menu object
if (config || !popupconfig[popup])
add_popup(popup, config);
config = popupconfig[popup] || {};
var ref = $(config.link ? config.link : '#'+popup+'link'),
pos = ref.offset();
if (ref.has('.inner'))
ref = ref.children('.inner');
// fire command with simulated mouse click event
return rcmail.command('menu-open',
{ menu:popup, show:show },
ref.get(0),
$.Event('click', { target:ref.get(0), pageX:pos.left, pageY:pos.top, clientX:pos.left, clientY:pos.top }));
}
/**
* Switch between short and full headers display in message preview
*/
function toggle_preview_headers()
{
$('#preview-shortheaders').toggle();
var full = $('#preview-allheaders').toggle(),
button = $('a#previewheaderstoggle');
// add toggle button to full headers table
if (full.is(':visible'))
button.attr('href', '#hide').removeClass('add').addClass('remove').attr('aria-expanded', 'true');
else
button.attr('href', '#details').removeClass('remove').addClass('add').attr('aria-expanded', 'false');
save_pref('previewheaders', full.is(':visible') ? '1' : '0');
}
/**
*
*/
function switch_view_mode(mode, force)
{
if (force || !$('#mail'+mode+'mode').hasClass('disabled')) {
$('#maillistmode, #mailthreadmode').removeClass('selected').attr('tabindex', '0').attr('aria-disabled', 'false');
$('#mail'+mode+'mode').addClass('selected').attr('tabindex', '-1').attr('aria-disabled', 'true');
}
}
/**** popup menu callbacks ****/
/**
* Handler for menu-open and menu-close events
*/
function menu_toggle(p)
{
if (p && p.name == 'messagelistmenu') {
show_listoptions(p);
}
else if (p) {
// adjust menu position according to config
var config = popupconfig[p.name] || {},
ref = $(config.link || '#'+p.name+'link'),
visible = p.obj && p.obj.is(':visible'),
above = config.above;
// fix position according to config
if (p.obj && visible && ref.length) {
var parent = ref.parent(),
win = $(window), pos;
if (parent.hasClass('dropbutton'))
ref = parent;
if (config.above || ref.hasClass('dropbutton')) {
pos = ref.offset();
p.obj.css({ left:pos.left+'px', top:(pos.top + (config.above ? -p.obj.height() : ref.outerHeight()))+'px' });
}
}
// add the right classes
if (p.obj && config.iconized) {
p.obj.children('ul').addClass('iconized');
}
// apply some data-attributes from menu config
if (p.obj && config.editable)
p.obj.attr('data-editable', 'true');
// trigger callback function
if (typeof config.callback == 'function') {
config.callback(visible, p);
}
}
}
function searchmenu(show)
{
if (show && rcmail.env.search_mods) {
var n, all,
obj = popups['searchmenu'],
list = $('input:checkbox[name="s_mods[]"]', obj),
mbox = rcmail.env.mailbox,
mods = rcmail.env.search_mods,
scope = rcmail.env.search_scope || 'base';
if (rcmail.env.task == 'mail') {
if (scope == 'all')
mbox = '*';
mods = mods[mbox] ? mods[mbox] : mods['*'];
all = 'text';
$('input:radio[name="s_scope"]').prop('checked', false).filter('#s_scope_'+scope).prop('checked', true);
}
else {
all = '*';
}
if (mods[all])
list.map(function() {
this.checked = true;
this.disabled = this.value != all;
});
else {
list.prop('disabled', false).prop('checked', false);
for (n in mods)
$('#s_mod_' + n).prop('checked', true);
}
}
}
function attachmentmenu(elem, event)
{
var id = elem.parentNode.id.replace(/^attach/, '');
$.each(['open', 'download', 'rename'], function() {
var action = this;
$('#attachmenu' + action).off('click').attr('onclick', '').click(function(e) {
return rcmail.command(action + '-attachment', id, this);
});
});
popupconfig.attachmentmenu.link = elem;
rcmail.command('menu-open', {menu: 'attachmentmenu', id: id}, elem, event);
}
function spellmenu(show, p)
{
var k, link, li,
lang = rcmail.spellcheck_lang(),
ul = $('ul', p.obj);
if (!ul.length) {
ul = $('<ul class="toolbarmenu selectable" role="menu">');
for (k in rcmail.env.spell_langs) {
li = $('<li role="menuitem">');
link = $('<a href="#'+k+'" tabindex="0"></a>').text(rcmail.env.spell_langs[k])
.addClass('active').data('lang', k)
.on('click keypress', function(e) {
if (e.type != 'keypress' || rcube_event.get_keycode(e) == 13) {
rcmail.spellcheck_lang_set($(this).data('lang'));
rcmail.hide_menu('spellmenu', e);
return false;
}
});
link.appendTo(li);
li.appendTo(ul);
}
ul.appendTo(p.obj);
}
// select current language
$('li', ul).each(function() {
var el = $('a', this);
if (el.data('lang') == lang)
el.addClass('selected').attr('aria-selected', 'true');
else if (el.hasClass('selected'))
el.removeClass('selected').removeAttr('aria-selected');
});
}
// append drop-icon to attachments list item (to invoke attachment menu)
function attachmentmenu_append(item)
{
item = $(item);
- if (!item.children('.drop').length)
- var label = rcmail.gettext('options');
- item.append($('<a>')
- .attr({'class': 'drop skip-content', tabindex: 0, 'aria-haspopup': true, title: label})
+ if (!item.children('.drop').length) {
+ var label = rcmail.gettext('options'),
+ fname = item.find('a.filename'),
+ tabindex = fname.attr('tabindex') || 0;
+
+ var button = $('<a>')
+ .attr({'class': 'drop skip-content', tabindex: tabindex, 'aria-haspopup': true, title: label})
.text(label)
.on('click keypress', function(e) {
if (e.type != 'keypress' || rcube_event.get_keycode(e) == 13) {
attachmentmenu(this, e);
return false;
}
- }));
+ });
+
+ if (fname.length)
+ button.insertAfter(fname);
+ else
+ button.appendTo(item);
+ }
}
/**
*
*/
function show_listoptions(p)
{
var $dialog = $('#listoptions');
// close the dialog
if ($dialog.is(':visible')) {
$dialog.dialog('close', p.originalEvent);
return;
}
// set form values
$('input[name="sort_col"][value="'+rcmail.env.sort_col+'"]').prop('checked', true);
$('input[name="sort_ord"][value="DESC"]').prop('checked', rcmail.env.sort_order == 'DESC');
$('input[name="sort_ord"][value="ASC"]').prop('checked', rcmail.env.sort_order != 'DESC');
$.each(['widescreen', 'desktop', 'list'], function() {
$('input[name="layout"][value="' + this + '"]').prop('checked', rcmail.env.layout == this);
});
$('#listoptions-columns', $dialog)[rcmail.env.layout == 'widescreen' ? 'hide' : 'show']();
// set checkboxes
$('input[name="list_col[]"]').each(function() {
$(this).prop('checked', $.inArray(this.value, rcmail.env.listcols) != -1);
});
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: true,
title: null,
open: function(e) {
setTimeout(function(){ $dialog.find('a, input:not(:disabled)').not('[aria-disabled=true]').first().focus(); }, 100);
},
close: function(e) {
$dialog.dialog('destroy').hide();
if (e.originalEvent && rcube_event.is_keyboard(e.originalEvent))
$('#listmenulink').focus();
},
minWidth: 500,
width: $dialog.width()+25
}).show();
}
/**
*
*/
function save_listoptions(p)
{
$('#listoptions').dialog('close');
if (rcube_event.is_keyboard(p.originalEvent))
$('#listmenulink').focus();
var sort = $('input[name="sort_col"]:checked').val(),
ord = $('input[name="sort_ord"]:checked').val(),
layout = $('input[name="layout"]:checked').val(),
cols = $('input[name="list_col[]"]:checked')
.map(function(){ return this.value; }).get();
rcmail.set_list_options(cols, sort, ord, rcmail.env.threading, layout);
}
/**
*
*/
function set_searchmod(elem)
{
var all, m, task = rcmail.env.task,
mods = rcmail.env.search_mods,
mbox = rcmail.env.mailbox,
scope = $('input[name="s_scope"]:checked').val();
if (scope == 'all')
mbox = '*';
if (!mods)
mods = {};
if (task == 'mail') {
if (!mods[mbox])
mods[mbox] = rcube_clone_object(mods['*']);
m = mods[mbox];
all = 'text';
}
else { //addressbook
m = mods;
all = '*';
}
if (!elem.checked)
delete(m[elem.value]);
else
m[elem.value] = 1;
// mark all fields
if (elem.value == all) {
$('input:checkbox[name="s_mods[]"]').map(function() {
if (this == elem)
return;
this.checked = true;
if (elem.checked) {
this.disabled = true;
delete m[this.value];
}
else {
this.disabled = false;
m[this.value] = 1;
}
});
}
rcmail.set_searchmods(m);
}
function set_searchscope(elem)
{
rcmail.set_searchscope(elem.value);
}
function push_contactgroup(p)
{
// lets the contacts list swipe to the left, nice!
var table = $('#contacts-table'),
scroller = table.parent().css('overflow', 'hidden');
table.clone()
.css({ position:'absolute', top:'0', left:'0', width:table.width()+'px', 'z-index':10 })
.appendTo(scroller)
.animate({ left: -(table.width()+5) + 'px' }, 300, 'swing', function(){
$(this).remove();
scroller.css('overflow', 'auto')
});
}
function pop_contactgroup(p)
{
// lets the contacts list swipe to the left, nice!
var table = $('#contacts-table'),
scroller = table.parent().css('overflow', 'hidden'),
clone = table.clone().appendTo(scroller);
table.css({ position:'absolute', top:'0', left:-(table.width()+5) + 'px', width:table.width()+'px', height:table.height()+'px', 'z-index':10 })
.animate({ left:'0' }, 300, 'linear', function(){
clone.remove();
$(this).css({ position:'relative', left:'0', width:'100%', height:'auto', 'z-index':1 });
scroller.css('overflow', 'auto')
});
}
/**
* Mail import dialog
*/
function import_dialog()
{
var content = $('#uploadform'),
dialog = content.clone().removeClass('popupdialog');
var save_func = function(e) {
return rcmail.command('import-messages', $(dialog.find('form')[0]));
};
rcmail.simple_dialog(dialog, rcmail.gettext('importmessages'), save_func, {
button: 'import',
closeOnEscape: true,
minWidth: 400
});
}
/**
*
*/
function show_header_row(which, updated)
{
var row = $('#compose-' + which);
if (row.is(':visible'))
return; // nothing to be done here
if (compose_headers[which] && !updated)
$('#_' + which).val(compose_headers[which]);
row.show();
$('#' + which + '-link').hide();
layout_composeview();
$('input,textarea', row).focus();
return false;
}
/**
*
*/
function hide_header_row(which)
{
// copy and clear field value
var field = $('#_' + which);
compose_headers[which] = field.val();
field.val('');
$('#compose-' + which).hide();
$('#' + which + '-link').show();
layout_composeview();
return false;
}
/**
* Fieldsets-to-tabs converter
*/
function init_tabs(elem, current)
{
var content = $(elem),
id = content.get(0).id,
fs = content.children('fieldset');
if (!fs.length)
return;
if (!id) {
id = 'rcmtabcontainer';
content.attr('id', id);
}
// create tabs container
var tabs = $('<ul>').addClass('tabsbar').prependTo(content);
// convert fildsets into tabs
fs.each(function(idx) {
var tab, a, elm = $(this),
legend = elm.children('legend'),
tid = id + '-t' + idx;
// create a tab
a = $('<a>').text(legend.text()).attr('href', '#' + tid);
tab = $('<li>').addClass('tablink');
// remove legend
legend.remove();
// link fieldset with tab item
elm.attr('id', tid);
// add the tab to container
tab.append(a).appendTo(tabs);
});
// use jquery UI tabs widget to do the interaction and styling
content.tabs({
active: current || 0,
heightStyle: 'content',
activate: function(e, ui) {resize(); }
});
}
/**
* Show about page as jquery UI dialog
*/
function show_about(elem)
{
var frame = $('<iframe>').attr({id: 'aboutframe', src: rcmail.url('settings/about'), frameborder: '0'});
h = Math.floor($(window).height() * 0.75),
buttons = {},
supportln = $('#supportlink');
if (supportln.length && (env.supporturl = supportln.attr('href')))
buttons[supportln.html()] = function(e){ env.supporturl.indexOf('mailto:') < 0 ? window.open(env.supporturl) : location.href = env.supporturl };
frame.dialog({
modal: true,
resizable: false,
closeOnEscape: true,
title: elem ? elem.title || elem.innerHTML : null,
close: function() {
frame.dialog('destroy').remove();
},
buttons: buttons,
width: 640,
height: h
}).width(640);
}
}
/**
* Roundcube Scroller class
*
* @deprecated Use treelist widget
*/
function rcube_scroller(list, top, bottom)
{
var ref = this;
this.list = $(list);
this.top = $(top);
this.bottom = $(bottom);
this.step_size = 6;
this.step_time = 20;
this.delay = 500;
this.top
.mouseenter(function() { if (rcmail.drag_active) ref.ts = window.setTimeout(function() { ref.scroll('down'); }, ref.delay); })
.mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); });
this.bottom
.mouseenter(function() { if (rcmail.drag_active) ref.ts = window.setTimeout(function() { ref.scroll('up'); }, ref.delay); })
.mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); });
this.scroll = function(dir)
{
var ref = this, size = this.step_size;
if (!rcmail.drag_active)
return;
if (dir == 'down')
size *= -1;
this.list.get(0).scrollTop += size;
this.ts = window.setTimeout(function() { ref.scroll(dir); }, this.step_time);
};
};
/**
* Roundcube UI splitter class
*
* @constructor
*/
function rcube_splitter(p)
{
this.p = p;
this.id = p.id;
this.horizontal = (p.orientation == 'horizontal' || p.orientation == 'h');
this.halfsize = (p.size !== undefined ? p.size : 10) / 2;
this.pos = p.start || 0;
this.min = p.min || 20;
this.offset = p.offset || 0;
this.relative = p.relative ? true : false;
this.drag_active = false;
this.render = p.render;
this.callback = p.callback;
var me = this;
rcube_splitter._instances[this.id] = me;
this.init = function()
{
this.p1 = $(this.p.p1);
this.p2 = $(this.p.p2);
this.parent = this.p1.parent();
// check if referenced elements exist, otherwise abort
if (!this.p1.length || !this.p2.length)
return;
// create and position the handle for this splitter
this.p1pos = this.relative ? this.p1.position() : this.p1.offset();
this.p2pos = this.relative ? this.p2.position() : this.p2.offset();
this.handle = $('<div>')
.attr('id', this.id)
.attr('unselectable', 'on')
.attr('role', 'presentation')
.addClass('splitter ' + (this.horizontal ? 'splitter-h' : 'splitter-v'))
.appendTo(this.parent)
.mousedown(onDragStart);
if (this.horizontal) {
var top = this.p1pos.top + this.p1.outerHeight();
this.handle.css({ left:'0px', top:top+'px' });
}
else {
var left = this.p1pos.left + this.p1.outerWidth();
this.handle.css({ left:left+'px', top:'0px' });
}
// listen to window resize on IE
if (bw.ie)
$(window).resize(onResize);
// read saved position from cookie
var cookie = this.get_cookie();
if (cookie && !isNaN(cookie)) {
this.pos = parseFloat(cookie);
this.resize();
}
else if (this.pos) {
this.resize();
this.set_cookie();
}
};
/**
* Set size and position of all DOM objects
* according to the saved splitter position
*/
this.resize = function()
{
if (this.horizontal) {
this.p1.css('height', Math.floor(this.pos - this.p1pos.top - Math.floor(this.halfsize)) + 'px');
this.p2.css('top', Math.ceil(this.pos + Math.ceil(this.halfsize) + 2) + 'px');
this.handle.css('top', Math.round(this.pos - this.halfsize + this.offset)+'px');
if (bw.ie) {
var new_height = parseInt(this.parent.outerHeight(), 10) - parseInt(this.p2.css('top'), 10);
this.p2.css('height', (new_height > 0 ? new_height : 0) + 'px');
}
}
else {
this.p1.css('width', Math.floor(this.pos - this.p1pos.left - Math.floor(this.halfsize)) + 'px');
this.p2.css('left', Math.ceil(this.pos + Math.ceil(this.halfsize)) + 'px');
this.handle.css('left', Math.round(this.pos - this.halfsize + this.offset + 3)+'px');
if (bw.ie) {
var new_width = parseInt(this.parent.outerWidth(), 10) - parseInt(this.p2.css('left'), 10) ;
this.p2.css('width', (new_width > 0 ? new_width : 0) + 'px');
}
}
this.p2.resize();
this.p1.resize();
// also resize iframe covers
if (this.drag_active) {
$('iframe').each(function(i, elem) {
var pos = $(this).offset();
$('#iframe-splitter-fix-'+i).css({ top: pos.top+'px', left: pos.left+'px', width:elem.offsetWidth+'px', height: elem.offsetHeight+'px' });
});
}
if (typeof this.render == 'function')
this.render(this);
};
/**
* Handler for mousedown events
*/
function onDragStart(e)
{
// disable text selection while dragging the splitter
if (bw.konq || bw.chrome || bw.safari)
document.body.style.webkitUserSelect = 'none';
me.p1pos = me.relative ? me.p1.position() : me.p1.offset();
me.p2pos = me.relative ? me.p2.position() : me.p2.offset();
me.drag_active = true;
// start listening to mousemove events
$(document).on('mousemove.' + this.id, onDrag).on('mouseup.' + this.id, onDragStop);
// hack messages list so it will propagate the mouseup event over the list
if (rcmail.message_list)
rcmail.message_list.drag_active = true;
// enable dragging above iframes
$('iframe').each(function(i, elem) {
$('<div>')
.attr('id', 'iframe-splitter-fix-'+i)
.addClass('iframe-splitter-fix')
.css({ background: '#fff',
width: elem.offsetWidth+'px', height: elem.offsetHeight+'px',
position: 'absolute', opacity: '0.001', zIndex: 1000
})
.css($(this).offset())
.appendTo('body');
});
};
/**
* Handler for mousemove events
*/
function onDrag(e)
{
if (!me.drag_active)
return false;
// with timing events dragging action is more responsive
window.clearTimeout(me.ts);
me.ts = window.setTimeout(function() { onDragAction(e); }, 1);
return false;
};
/**
* Dragging action (see onDrag())
*/
function onDragAction(e)
{
var pos = rcube_event.get_mouse_pos(e);
if (me.relative) {
var parent = me.parent.offset();
pos.x -= parent.left;
pos.y -= parent.top;
}
if (me.horizontal) {
if (((pos.y - me.halfsize) > me.p1pos.top) && ((pos.y + me.halfsize) < (me.p2pos.top + me.p2.outerHeight()))) {
me.pos = Math.max(me.min, pos.y - Math.max(0, me.offset));
if (me.pos > me.min)
me.pos = Math.min(me.pos, me.parent.height() - me.min);
me.resize();
}
}
else {
if (((pos.x - me.halfsize) > me.p1pos.left) && ((pos.x + me.halfsize) < (me.p2pos.left + me.p2.outerWidth()))) {
me.pos = Math.max(me.min, pos.x - Math.max(0, me.offset));
if (me.pos > me.min)
me.pos = Math.min(me.pos, me.parent.width() - me.min);
me.resize();
}
}
me.p1pos = me.relative ? me.p1.position() : me.p1.offset();
me.p2pos = me.relative ? me.p2.position() : me.p2.offset();
};
/**
* Handler for mouseup events
*/
function onDragStop(e)
{
// resume the ability to highlight text
if (bw.konq || bw.chrome || bw.safari)
document.body.style.webkitUserSelect = 'auto';
// cancel the listening for drag events
$(document).off('.' + me.id);
me.drag_active = false;
if (rcmail.message_list)
rcmail.message_list.drag_active = false;
// remove temp divs
$('div.iframe-splitter-fix').remove();
me.set_cookie();
if (typeof me.callback == 'function')
me.callback(me);
return bw.safari ? true : rcube_event.cancel(e);
};
/**
* Handler for window resize events
*/
function onResize(e)
{
if (me.horizontal) {
var new_height = parseInt(me.parent.outerHeight(), 10) - parseInt(me.p2[0].style.top, 10);
me.p2.css('height', (new_height > 0 ? new_height : 0) +'px');
}
else {
var new_width = parseInt(me.parent.outerWidth(), 10) - parseInt(me.p2[0].style.left, 10);
me.p2.css('width', (new_width > 0 ? new_width : 0) + 'px');
}
};
/**
* Get saved splitter position from cookie
*/
this.get_cookie = function()
{
return window.UI ? UI.get_pref(this.id) : null;
};
/**
* Saves splitter position in cookie
*/
this.set_cookie = function()
{
if (window.UI)
UI.save_pref(this.id, this.pos);
};
} // end class rcube_splitter
// static getter for splitter instances
rcube_splitter._instances = {};
rcube_splitter.get_instance = function(id)
{
return rcube_splitter._instances[id];
};
// @license-end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Wed, Feb 4, 5:53 AM (9 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
427594
Default Alt Text
(266 KB)
Attached To
Mode
R3 roundcubemail
Attached
Detach File
Event Timeline
Log In to Comment