/** * 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; 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; // 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, 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(true); 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) { if (title = $('h1.voice:first').text()) { parent.$('.header > .header-title', layout.content).text(title); } } else { title = $('.boxtitle:first', layout.content).detach().text(); if (!title) { title = $('h1.voice:first').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() { hidden.val(this.type != 'checkbox' || this.checked ? $(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); }); }); // 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); }) .addEventListener('managesieve.insertrow', function(o) { console.log(o); bootstrap_style(o.obj); }); 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) { if (!context) { context = document; } $('input.button,button', context).addClass('btn').not('.btn-primary,.primary,.mainaction').addClass('btn-secondary'); $('input.button.mainaction,button.primary,button.mainaction', context).addClass('btn-primary'); $('button.btn.delete', context).addClass('btn-danger'); $.each(['warning', 'error'], function() { var type = this; $('.box' + type, context).each(function() { message_displayed({object: this, type: type}); }); }); // Forms $('input:not(.button,[type=file],[type=radio],[type=checkbox]),select,textarea', $('.propform', context)).addClass('form-control'); $('[type=checkbox]', $('.propform', context)).addClass('form-check-input'); $('table.propform > tbody > tr', context).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'); } // style some multi-input fields if (last.children('.datepicker') && last.children('input').length == 2) { last.addClass('datetime'); } } row.addClass(row_classes.join(' ')); }); // Special input + anything entry $('td.input-group', context).each(function() { $(this).children(':not(:first)').addClass('input-group-addon'); }); // Other forms, e.g. Contact advanced search $('fieldset.propform > .contactfieldgroup', context).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,textarea', this).addClass('form-control'); }); // 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($('
')); $('input,select,textarea', this).addClass('form-control').wrap($('
')); }); }); $('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 // TODO: Should we use Accordion widget instead on mobile? $('form.tabbed,div.tabbed', context).each(function(idx, item) { var tabs = [], nav = $('