/**
* Roundcube webmail functions for the Elastic skin
*
* Copyright (c) 2017, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*
* @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
*/
"use strict";
function rcube_elastic_ui()
{
var ref = this,
mode = 'normal', // one of: large, normal, small, phone
touch = false,
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
},
small_screen_config: {
standard_windows: true,
message_extwin: false,
compose_extwin: false,
help_open_extwin: false
}
},
menus = {},
content_buttons = [],
layout = {
menu: $('#layout > .menu'),
sidebar: $('#layout > .sidebar'),
list: $('#layout > .list'),
content: $('#layout > .content'),
},
buttons = {
menu: $('a.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.about_dialog = about_dialog;
this.spellmenu = spellmenu;
this.searchmenu = searchmenu;
this.headersmenu = headersmenu;
this.attachmentmenu = attachmentmenu;
this.mailtomenu = mailtomenu;
this.show_list = show_list;
this.show_sidebar = show_sidebar;
// 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 content_buttons = [],
is_framed = rcmail.is_framed();
// Initialize search forms (in list headers)
$('.header > .searchbar').each(function() { searchbar_init(this); });
$('.header > .searchfilterbar').each(function() { searchfilterbar_init(this); });
// Intercept jQuery-UI dialogs to re-style them
if ($.ui) {
$.widget('ui.dialog', $.ui.dialog, {
open: function() {
this._super();
dialog_open(this);
return this;
}
});
}
// menu/sidebar/list button
buttons.menu.on('click', function() { show_menu(); 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(); return false; });
$('body').on('click', function() { if (mode == 'phone') layout.menu.addClass('hidden'); });
// Set content frame title in parent window (exclude ext-windows and dialog frames)
if (is_framed && !rcmail.env.extwin && !parent.$('.ui-dialog:visible').length) {
var title = $('h1.voice:first').text();
if (title) {
parent.$('.header > .header-title', layout.content).text(title);
}
}
else {
var title = $('.boxtitle:first', layout.content).detach().text();
if (title) {
$('.header > .header-title', layout.content).text(title);
}
}
// Move some buttons to the toolbar
$('a[data-content-button]').each(function() {
var target = $(this),
button = target.clone(),
target_id = target.attr('id'),
button_id = target_id + '-clone';
content_buttons.push(
button.attr({'onclick': '', id: button_id, title: target.text()})
.on('click', function(e) { target.click(); })
.text('')
);
// Register the button to get active state updates
register_cloned_button(target_id, button_id);
});
// Move form buttons from the content frame into the frame header (on parent window)
// TODO: Active button state
$('.formbuttons').children().each(function() {
var target = $(this);
// skip non-content buttons
if (!is_framed && !target.parents('.content').length) {
return;
}
if (target.is('.cancel')) {
target.addClass('hidden');
return;
}
var button = target.clone();
content_buttons.push(
button.attr({'onclick': '', disabled: false, id: button.attr('id') + '-clone', title: target.text()})
.data('target', target)
.on('click', function(e) { target.click(); })
.text('')
);
});
if (content_buttons.length) {
if (is_framed) {
if (parent.UI) {
parent.UI.register_content_buttons(content_buttons);
}
}
else {
register_content_buttons(content_buttons);
}
}
$('[data-recipient-input]').each(function() { recipient_input(this); });
$('.image-upload').each(function() { image_upload_input(this); });
// 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 for outside of the main form
// Because IE/Edge does not support 'form' attribute we'll copy
// inputs into the main form 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 = $('')
.attr({type: 'hidden', name: $(this).attr('name')})
.appendTo(rcmail.gui_objects.messageform);
$(this).on('change', function() {
if (this.type == 'checkbox') {
hidden.attr('checked', $(this).attr('checked'));
}
else {
hidden.val($(this).val());
}
}).change();
});
// Add HTML/Plain tabs (switch) on top of textarea with TinyMCE editor
$('textarea[data-html-editor]').each(function() { html_editor_init(this); });
$('#dragmessage-menu,#dragcontact-menu').each(function() {
rcmail.gui_object('dragmenu', this.id);
});
// Taskmenu items added by plugins do not use elastic classes (e.g help plugin)
// it's for larry skin compat. We'll assign 'button', '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 (data = button.data) {
if (data.sel) {
data.sel += ' button ' + name;
data.sel = data.sel.replace('button-selected', 'selected');
}
if (data.act) {
data.act += ' button ' + name;
}
rcmail.buttons[button.command][button.index] = data;
rcmail.init_button(button.command, data);
$(this).addClass('button ' + name);
$('.button-inner', this).addClass('inner');
}
}
});
// 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
$('a[data-hidden],button[data-hidden]').each(function() {
var parent = $(this).parent('li'),
sizes = $(this).data('hidden').split(',');
$(parent.length ? parent : this).addClass('hidden-' + sizes.join(' hidden-'));
});
// 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); });
});
// The same for some other checkboxes
$('table.propform input[type=checkbox], .form-check > input, .popupmenu.form input[type=checkbox]')
.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');
}
});
}
};
/**
* Moves form buttons into content frame toolbar (for mobile)
*/
function register_content_buttons(buttons)
{
// we need these buttons really only in phone mode
if (/*mode == 'phone' && */layout.content.length && buttons && buttons.length) {
var header = layout.content.children('.header');
if (header.length) {
var toolbar = header.children('.buttons');
if (!toolbar.length) {
var menu = $('a.toolbar-menu-button', header);
toolbar = $('');
if (menu.length) {
menu.before(toolbar);
}
else {
toolbar.appendTo(header);
}
}
content_buttons = [];
$.each(buttons, function() {
if (this.data('target')) {
content_buttons.push(this.data('target'));
}
});
toolbar.html('').append(buttons);
resize();
}
}
};
/**
* Registers cloned button
*/
function register_cloned_button(old_id, new_id)
{
var button = find_button(old_id);
if (button) {
rcmail.register_button(button.command, new_id,
button.data.type, button.data.act, button.data.sel, button.data.over);
}
};
/**
* 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('init', init);
};
/**
* rcmail 'init' event handler
*/
function init()
{
// Enable checkbox selection on list widgets
$('table[data-list]').each(function() {
var list = $(this).data('list');
if (rcmail[list] && rcmail[list].multiselect) {
rcmail[list].checkbox_selection = true;
}
});
// Add menu link for each attachment
$('#attachment-list > li').each(function() {
attachmentmenu_append(this);
});
rcmail.addEventListener('fileappended', function(e) { if (e.attachment.complete) attachmentmenu_append(e.item); });
rcmail.init_pagejumper('.pagenav > input');
if (rcmail.task == 'mail') {
// In compose window we do not provide "Back' button, instead
// we modify the Mail button in the task menu to act like it (i.e. calls 'list' command)
if (rcmail.env.action == 'compose' && !rcmail.env.extwin) {
$('a.button.mail', layout.menu).attr('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);
});
}
}
rcmail.env.thread_padding = '1.5rem';
// In devel mode we have to wait until all styles are aplied by less
if (rcmail.env.devel_mode) {
setTimeout(resize, 1000);
}
};
/**
* Apply bootstrap classes to html elements
*/
function bootstrap_style(context)
{
$('input.button,button', context || document).addClass('btn').not('.btn-primary,.primary,.mainaction').addClass('btn-secondary');
$('input.button.mainaction,button.primary,button.mainaction', context || document).addClass('btn-primary');
$('button.btn.delete').addClass('btn-danger');
$.each(['warning', 'error'], function() {
var type = this;
$('.box' + type, context).each(function() {
message_displayed({object: this, type: type});
});
});
// Forms
$('input,select,textarea', $('table.propform')).not('[type=checkbox]').addClass('form-control');
$('[type=checkbox]', $('table.propform')).addClass('form-check-input');
$('table.propform > tbody > tr').each(function() {
var first, last, row = $(this),
row_classes = ['form-group', 'row'],
cells = row.children('td');
if (cells.length == 2) {
first = cells.first();
last = cells.last();
$('label', first).addClass('col-form-label');
first.addClass('col-sm-4');
last.addClass('col-sm-8');
if (last.find('[type=checkbox]').length) {
row_classes.push('form-check');
}
else if (!last.find('input,textarea,radio,select').length) {
last.addClass('form-control-plaintext');
}
}
row.addClass(row_classes.join(' '));
});
// Other forms, e.g. Contact advanced search
$('fieldset.propform > .contactfieldgroup').each(function() {
$('.row', this).addClass('form-group').each(function() {
$('div:first', this).addClass('col-sm-4');
$('div:last', this).addClass('col-sm-8');
});
$('.label', this).addClass('col-form-label');
$('input,select').addClass('form-control');
});
// 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
// TODO: Should we use Accordion widget instead on mobile?
$('form.tabbed,div.tabbed', context).each(function(idx, item) {
var tabs = [], nav = $('