/** * 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'), }; // Public methods this.register_frame_buttons = register_frame_buttons; this.menu_hide = menu_hide; this.about_dialog = about_dialog; this.spellmenu = spellmenu; this.searchmenu = searchmenu; this.headersmenu = headersmenu; this.attachmentmenu = attachmentmenu; // 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(); // Update layout after initialization resize(); // Setup various UI elements setup(); /** * Setup procedure */ function setup() { // Initialize search forms (in list headers) $('.header > .searchbar').each(function() { searchbar_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; }); $('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 (rcmail.is_framed() && !rcmail.env.extwin && !parent.$('.ui-dialog:visible').length) { var title = $('h1.voice:first').text(); if (title) { parent.$('#content > .header > .header-title').text(title); } } else { var title = $('#content .boxtitle:first').detach().text(); if (title) { $('#content > .header > .header-title').text(title); } } // Move form buttons from the content frame into the frame header (on parent window) // TODO: Active button state var form_buttons = []; $('.formbuttons').children(':not(.cancel)').each(function() { var target = $(this); // skip non-content buttons if (!rcmail.is_framed() && !target.parents('.content').length) { return; } var button = target.clone(); form_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 (form_buttons.length) { if (rcmail.is_framed()) { if (parent.UI) { parent.UI.register_frame_buttons(form_buttons); } } else { register_frame_buttons(form_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).val()); }).change(); }); $('#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 button = this, id = button.id, name = RegExp.$1; $.each(rcmail.buttons, function(i, v) { $.each(v, function(j, data) { if (data.id == id) { if (data.sel) { data.sel += ' button ' + name; data.sel = data.sel.replace('button-selected', 'selected'); } if (data.act) { data.act += ' button ' + name; } rcmail.buttons[i][j] = data; rcmail.init_button(i, data); $(button).addClass('button ' + name); $('.button-inner', button).addClass('inner'); } }); }); } }); // buttons that should be hidden on small screen devices $('a[data-hidden-small],button[data-hidden-small]').each(function() { var parent = $(this).parent('li'); $(parent.length ? parent : this).addClass('hidden-small'); }); }; /** * Moves form buttons into content frame toolbar (for mobile) */ function register_frame_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() { content_buttons.push(this.data('target')); }); toolbar.html('').append(buttons); resize(); } } }; /** * 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); }); }; /** * 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 col-form-label'); 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(' ')); }); // 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 = $('