|
|
|
/**
|
|
|
|
* 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 prefs, 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;
|
|
|
|
this.toggle_list_selection = toggle_list_selection;
|
|
|
|
this.get_screen_mode = get_screen_mode;
|
|
|
|
this.is_mobile = is_mobile;
|
|
|
|
this.is_touch = is_touch;
|
|
|
|
|
|
|
|
// 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 = layout.content.find('.boxtitle').first().detach().text();
|
|
|
|
|
|
|
|
if (!title) {
|
|
|
|
title = $('h1.voice').first().text();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (title) {
|
|
|
|
layout.content.find('.header > .header-title').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 switcher 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('clonerow', pretty_checkbox_fix)
|
|
|
|
.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});
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.env.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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize column resizers (must be after floating buttons)
|
|
|
|
if (layout.sidebar.length) {
|
|
|
|
splitter_init(layout.sidebar);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (layout.list.length) {
|
|
|
|
splitter_init(layout.list);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 selection disabled', role: 'button', title: rcmail.gettext('select')})
|
|
|
|
.on('click', function() { UI.toggle_list_selection(this, table.attr('id')); })
|
|
|
|
.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();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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') {
|
|
|
|
rcmail.addEventListener('compose-encrypted', function(e) {
|
|
|
|
$("a.mode-html, button.attach").prop('disabled', e.active);
|
|
|
|
$('a.attach, a.responses:not(.edit)')[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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// In compose/preview 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 && (rcmail.env.action == 'compose' || rcmail.env.action == 'show')) {
|
|
|
|
$('a.mail', layout.menu).attr({
|
|
|
|
'aria-disabled': false,
|
|
|
|
onclick: "return rcmail.command('list','',this,event);"
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
// Re-focus the focused input field on mail compose
|
|
|
|
if (rcmail.env.compose_focus_elem) {
|
|
|
|
$(rcmail.env.compose_focus_elem).focus();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
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;
|
|
|
|
var col_sizes = ['sm', 4, 8];
|
|
|
|
|
|
|
|
if ($(this).attr('class').match(/cols-([a-z]+)-(\d)-(\d)/)) {
|
|
|
|
col_sizes = [RegExp.$1, RegExp.$2, RegExp.$3];
|
|
|
|
}
|
|
|
|
|
|
|
|
$(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-' + col_sizes[0] + '-' + col_sizes[1]);
|
|
|
|
last.addClass('col-' + col_sizes[0] + '-' + col_sizes[2]);
|
|
|
|
|
|
|
|
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 = String($(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()
|
|
|
|
{
|
|
|
|
if (!layout.list.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var last_selected = env.last_selected,
|
|
|
|
title_reset = function(title) {
|
|
|
|
if (typeof title !== 'string' || !title.length) {
|
|
|
|
title = $('h1.voice').text() || $('title').text() || '';
|
|
|
|
}
|
|
|
|
|
|
|
|
layout.content.find('.header > .header-title').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)
|
|
|
|
{
|
|
|
|
var onload = [],
|
|
|
|
is_editor = $('#' + o.id).parent().is('.html-editor');
|
|
|
|
|
|
|
|
// 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
|
|
|
|
onload.push(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'); });
|
|
|
|
}
|
|
|
|
|
|
|
|
if (is_editor) {
|
|
|
|
o.config.toolbar = 'plaintext | ' + o.config.toolbar;
|
|
|
|
// Use setup_callback, we can't use editor-load event
|
|
|
|
o.config.setup_callback = function(ed) {
|
|
|
|
ed.addButton('plaintext', {
|
|
|
|
tooltip: rcmail.gettext('plaintoggle'),
|
|
|
|
icon: 'plaintext',
|
|
|
|
onclick: function(e) {
|
|
|
|
if (rcmail.command('toggle-editor', {id: ed.id, html: false}, '', e.originalEvent)) {
|
|
|
|
$('#' + ed.id).parent().removeClass('ishtml');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
rcmail.addEventListener('editor-load', function(e) {
|
|
|
|
$.each(onload, function() { this(e.ref.editor); });
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
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']});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
function toggle_list_selection(obj, list_id)
|
|
|
|
{
|
|
|
|
if ($(obj).is('.active')) {
|
|
|
|
$('#' + list_id).toggleClass('withselection');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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');
|
|
|
|
$('.toolbar a.reply-all').attr('title', label).find('.inner').text(label);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'compose-encrypted':
|
|
|
|
// show the toolbar button for Mailvelope
|
|
|
|
if (args.status) {
|
|
|
|
$('.toolbar a.encrypt').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;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get current screen mode
|
|
|
|
*/
|
|
|
|
function get_screen_mode()
|
|
|
|
{
|
|
|
|
return mode;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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'](); });
|
|
|
|
|
|
|
|
rcmail.triggerEvent('skin-resize', { mode: mode })
|
|
|
|
};
|
|
|
|
|
|
|
|
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:not(.position-absolute)').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.length) {
|
|
|
|
$('.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 unread_button = $(),
|
|
|
|
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');
|
|
|
|
unread_button[rcmail.gui_objects.search_filter && $(rcmail.gui_objects.search_filter).val() == 'UNSEEN' ? 'addClass' : 'removeClass']('selected');
|
|
|
|
};
|
|
|
|
|
|
|
|
// Add Unread filter button
|
|
|
|
if (input.is('#mailsearchform')) {
|
|
|
|
unread_button = $('<a>')
|
|
|
|
.attr({'class': 'button unread', href: '#', role: 'button', title: rcmail.gettext('showunread')})
|
|
|
|
.on('click', function(e) {
|
|
|
|
$(rcmail.gui_objects.search_filter).val($(e.target).is('.selected') ? 'ALL' : 'UNSEEN');
|
|
|
|
rcmail.command('search');
|
|
|
|
})
|
|
|
|
.insertBefore(options_button);
|
|
|
|
}
|
|
|
|
|
|
|
|
options_button.on('click', function(e) {
|
|
|
|
var id = $(this).data('target'),
|
|
|
|
options = $('#' + id),
|
|
|
|
open = $(bar).is('.open');
|
|
|
|
|
|
|
|
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.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)
|
|
|
|
.addEventListener('beforelist', close_func)
|
|
|
|
.addEventListener('afterlist', update_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
|
|
|
|
layout.content.find('.header > .menu').each(function() {
|
|
|
|
var toolbar = $(this);
|
|
|
|
|
|
|
|
toolbar.children().each(function() { button_func(this, items); });
|
|
|
|
toolbar.remove();
|
|
|
|
});
|
|
|
|
|
|
|
|
// convert list toolbar to a popup list
|
|
|
|
layout.list.find('.header > .menu').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);
|
|
|
|
|
|
|
|
// bind toolbar menu with the menu button in the list header
|
|
|
|
layout.list.find('a.toolbar-menu-button').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.
|
|
|
|
.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;
|
|
|
|
|
|
|
|
// Sanity check, make sure we have some content to show
|
|
|
|
if (!content.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
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('.dropdown').length) {
|
|
|
|
var label = rcmail.gettext('options'),
|
|
|
|
fname = item.find('a.filename');
|
|
|
|
|
|
|
|
var button = $('<a>').attr({
|
|
|
|
href: '#',
|
|
|
|
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));
|
|
|
|
|
|
|
|
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, selection = '',
|
|
|
|
input_len_update = function() {
|
|
|
|
input.css('width', Math.max(5, input.val().length * 15 + 10));
|
|
|
|
},
|
|
|
|
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());
|
|
|
|
|
|
|
|
apply_func();
|
|
|
|
},
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
input.val(result.text);
|
|
|
|
apply_func();
|
|
|
|
input_len_update();
|
|
|
|
|
|
|
|
return result.recipients.length > 0;
|
|
|
|
},
|
|
|
|
parse_func = function(e, ac) {
|
|
|
|
var last, paste, value = this.value;
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
if (e.type == 'paste') {
|
|
|
|
// pasted text
|
|
|
|
paste = (e.originalEvent.clipboardData || window.clipboardData).getData('text') || '';
|
|
|
|
// insert pasted text in place of the selection (or just cursor position)
|
|
|
|
value = value.substring(0, this.selectionStart) + paste + value.substring(this.selectionEnd);
|
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
// #7231: When clicking on autocompletion list a change event
|
|
|
|
// is fired twice. We have to remove last recipient box if it is
|
|
|
|
// the same recpient (with incomplete email address).
|
|
|
|
// FIXME: Anyone with a better solution?
|
|
|
|
else if (ac) {
|
|
|
|
last = list.find('li.recipient').last();
|
|
|
|
if (last.length && this.value.indexOf(last.text().replace(/[ ,]+$/, '')) > -1) {
|
|
|
|
last.remove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
update_func(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 ac-input rounded-left')
|
|
|
|
.append($('<li>').append(input))
|
|
|
|
// "selection" hack to allow text selection in the recipient box or multiple boxes (#7129)
|
|
|
|
.on('mouseup', function () { selection = window.getSelection().toString(); })
|
|
|
|
.on('click', function() { if (!selection.length) input.focus(); })
|
|
|
|
.sortable({
|
|
|
|
appendTo: document.body,
|
|
|
|
items: "> .recipient",
|
|
|
|
connectWith: '.recipient-input',
|
|
|
|
receive: function(event, ui) {
|
|
|
|
input.parent().appendTo(list);
|
|
|
|
input.trigger($.Event('keydown', { key: "," }));
|
|
|
|
if (ui.sender) {
|
|
|
|
ui.sender.find('input').trigger($.Event('keydown', { key: "," }));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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(); e.preventDefault(); })
|
|
|
|
.on('change', function() {
|
|
|
|
$('li.recipient', list).remove();
|
|
|
|
input.val(this.value).change();
|
|
|
|
})
|
|
|
|
// copy and parse the value already set
|
|
|
|
.change();
|
|
|
|
|
|
|
|
// Init autocompletion
|
|
|
|
rcmail.init_address_input_events(input);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fix pretty checkbox input in a cloned element
|
|
|
|
*/
|
|
|
|
function pretty_checkbox_fix(params)
|
|
|
|
{
|
|
|
|
var id, input = $(params.row).find('input[id^=icochk]');
|
|
|
|
|
|
|
|
if (input.length) {
|
|
|
|
id = 'icochk' + (++env.checkboxes);
|
|
|
|
input.attr('id', id).next('label').attr('for', id);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 last_char, last_index = -1, items = [], index = [],
|
|
|
|
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);
|
|
|
|
index.push(this.disabled ? '' : label.charAt(0).toLowerCase());
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
link.html(' '); // link can't be empty
|
|
|
|
index.push('');
|
|
|
|
}
|
|
|
|
|
|
|
|
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, char, last, 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';
|
|
|
|
// no-break
|
|
|
|
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
|
|
|
|
|
|
|
|
default:
|
|
|
|
// A letter key has been pressed, search mode
|
|
|
|
char = e.originalEvent.key;
|
|
|
|
|
|
|
|
if (char && char.length == 1) {
|
|
|
|
char = char.toLowerCase();
|
|
|
|
|
|
|
|
if (last_char != char) {
|
|
|
|
last_index = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
last = index.indexOf(char, last_index + 1);
|
|
|
|
|
|
|
|
if (last > -1 || (last = index.indexOf(char)) > -1) {
|
|
|
|
list.find('a').eq(last).focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
last_char = char;
|
|
|
|
last_index = last;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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();
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
// Find the selected item, focus it
|
|
|
|
var selected = list.find('a.selected').first();
|
|
|
|
if (selected.focus().length) {
|
|
|
|
var list_parent = list.parent();
|
|
|
|
|
|
|
|
// try to scroll the list so focused element is in center
|
|
|
|
last_index = list.find('a').index(selected[0]);
|
|
|
|
last_char = index[last_index];
|
|
|
|
|
|
|
|
if (last_index > 5) {
|
|
|
|
list_parent.scrollTop(list_parent.scrollTop() + list_parent.height()/2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// focus first active element on the list
|
|
|
|
else 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 class="hidden"></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(),
|
|
|
|
plain_btn = $('<a class="mce-i-html" href="#" tabindex="-1"></a>')
|
|
|
|
.attr('title', rcmail.gettext('htmltoggle'))
|
|
|
|
.on('click', function(e) {
|
|
|
|
if (rcmail.command('toggle-editor', {id: editor.attr('id'), html: true}, '', e.originalEvent)) {
|
|
|
|
parent.addClass('ishtml');
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.on('keydown', function(e) {
|
|
|
|
if (e.which == 9) { // TAB
|
|
|
|
editor.focus();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
toolbar = $('<div class="editor-toolbar">').append(plain_btn);
|
|
|
|
|
|
|
|
if (parent.is('td')) {
|
|
|
|
sw = $('input[type="checkbox"]', parent.parent().next());
|
|
|
|
is_table = true;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
sw = editor.next('select.hidden');
|
|
|
|
}
|
|
|
|
|
|
|
|
// make the textarea autoresizeable
|
|
|
|
textarea_autoresize_init(editor);
|
|
|
|
|
|
|
|
// sanity check
|
|
|
|
if (sw.length != 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
parent.addClass('html-editor');
|
|
|
|
|
|
|
|
editor.after(toolbar).data('control', sw)
|
|
|
|
.on('keydown', function(e) {
|
|
|
|
// ALT + F10 is the way to access toolbar in TinyMCE, let's do the same for plain editor
|
|
|
|
if (e.altKey && e.which == 121) {
|
|
|
|
plain_btn.focus();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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 depending on it's content length.
|
|
|
|
* The way there's no vertical scrollbar.
|
|
|
|
*/
|
|
|
|
function textarea_autoresize_init(textarea)
|
|
|
|
{
|
|
|
|
var padding = parseInt($(textarea).css('padding-top')) + parseInt($(textarea).css('padding-bottom')) + 2,
|
|
|
|
// FIXME: Is there a better way to get initial height of the textarea?
|
|
|
|
// At this moment clientHeight/offsetHeight is 0.
|
|
|
|
min_height = ($(textarea)[0].rows || 5) * 21,
|
|
|
|
resize = function(e) {
|
|
|
|
if (this.scrollHeight - padding <= min_height) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// To fix scroll-jump issue in Edge we'll find the scrolling parent
|
|
|
|
// and re-apply scrollTop value after we reset textarea height
|
|
|
|
var scroll_element, scroll_pos = 0;
|
|
|
|
$(e.target).parents().each(function() {
|
|
|
|
if (this.scrollTop > 0) {
|
|
|
|
scroll_element = this;
|
|
|
|
scroll_pos = this.scrollTop;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var oldHeight = $(this).outerHeight();
|
|
|
|
$(this).outerHeight(0);
|
|
|
|
var newHeight = Math.max(min_height, this.scrollHeight);
|
|
|
|
$(this).outerHeight(oldHeight);
|
|
|
|
if (newHeight !== oldHeight) {
|
|
|
|
$(this).height(newHeight);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (scroll_pos) {
|
|
|
|
scroll_element.scrollTop = scroll_pos;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
$(textarea).on('input', resize).trigger('input');
|
|
|
|
};
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a splitter (resizing) element on a layout column
|
|
|
|
*/
|
|
|
|
function splitter_init(node)
|
|
|
|
{
|
|
|
|
// Use id of the list element, if exists, as a part of the key, instead of action.column-id
|
|
|
|
// This way e.g. the sidebar in Settings is always the same width for all Settings' pages
|
|
|
|
var list_id = node.find('.scroller .listing').first().attr('id'),
|
|
|
|
key = rcmail.env.task + '.' + (list_id || (rcmail.env.action + '.' + node.attr('id'))),
|
|
|
|
pos = get_pref(key),
|
|
|
|
reverted = node.is('.sidebar-right'),
|
|
|
|
set_width = function(width) {
|
|
|
|
node.css({
|
|
|
|
width: Math.max(100, width),
|
|
|
|
// reset default properties
|
|
|
|
// 'min-width': 100,
|
|
|
|
flex: 'none'
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!node[reverted ? 'prev' : 'next']().length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$('<div class="column-resizer">')
|
|
|
|
.appendTo(node)
|
|
|
|
.on('mousedown', function(e) {
|
|
|
|
var ts, splitter = $(this), offset = node.position().left;
|
|
|
|
|
|
|
|
// Makes col-resize cursor follow the mouse pointer on dragging
|
|
|
|
// and fixes issues related to iframes
|
|
|
|
splitter.width(10000).css(reverted ? 'left' : 'right', -5000);
|
|
|
|
|
|
|
|
// Disable selection on document while dragging
|
|
|
|
// It can happen when you move mouse out of window, on top
|
|
|
|
document.body.style.userSelect = 'none';
|
|
|
|
|
|
|
|
// Start listening to mousemove events
|
|
|
|
$(document)
|
|
|
|
.on('mousemove.resizer', function(e) {
|
|
|
|
// Use of timeouts makes the move more smooth in Chrome
|
|
|
|
clearTimeout(ts);
|
|
|
|
ts = setTimeout(function() {
|
|
|
|
// For left-side-splitter we need the current offset
|
|
|
|
if (reverted) {
|
|
|
|
offset = node.position().left;
|
|
|
|
}
|
|
|
|
var cursor_position = rcube_event.get_mouse_pos(e).x,
|
|
|
|
width = reverted ? node.width() + (offset - cursor_position) : cursor_position - offset;
|
|
|
|
|
|
|
|
set_width(width);
|
|
|
|
}, 5);
|
|
|
|
})
|
|
|
|
.on('mouseup.resizer', function() {
|
|
|
|
// Remove registered events
|
|
|
|
$(document).off('.resizer');
|
|
|
|
$('iframe').off('.resizer');
|
|
|
|
document.body.style.userSelect = 'auto';
|
|
|
|
|
|
|
|
// Set back the splitter width to normal
|
|
|
|
splitter.width(6).css(reverted ? 'left' : 'right', -3);
|
|
|
|
|
|
|
|
// Save the current position (width)
|
|
|
|
save_pref(key, node.width());
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
if (pos) {
|
|
|
|
set_width(pos);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get preference stored in browser
|
|
|
|
*/
|
|
|
|
function get_pref(key)
|
|
|
|
{
|
|
|
|
if (!prefs) {
|
|
|
|
prefs = rcmail.local_storage_get_item('prefs.elastic', {});
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.elastic', 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.elastic', prefs)) {
|
|
|
|
// store value in cookie
|
|
|
|
var exp = new Date();
|
|
|
|
exp.setYear(exp.getFullYear() + 1);
|
|
|
|
rcmail.set_cookie(key, val, exp);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|