Page MenuHomePhorge

No OneTemporary

Size
162 KB
Referenced Files
None
Subscribers
None
diff --git a/program/js/list.js b/program/js/list.js
index edfb19065..a4b800131 100644
--- a/program/js/list.js
+++ b/program/js/list.js
@@ -1,1933 +1,1960 @@
/**
* Roundcube List Widget
*
* This file is part of the Roundcube Webmail client
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
* Copyright (c) 2005-2014, The Roundcube Dev Team
*
* The JavaScript code in this page is free software: you can
* redistribute it and/or modify it under the terms of the GNU
* General Public License (GNU GPL) as published by the Free Software
* Foundation, either version 3 of the License, or (at your option)
* any later version. The code is distributed WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Charles McNulty <charles@charlesmcnulty.com>
*
* @requires jquery.js, common.js
*/
/**
* Roundcube List Widget class
* @constructor
*/
function rcube_list_widget(list, p)
{
// static contants
this.ENTER_KEY = 13;
this.DELETE_KEY = 46;
this.BACKSPACE_KEY = 8;
this.list = list ? list : null;
this.tagname = this.list ? this.list.nodeName.toLowerCase() : 'table';
this.id_regexp = /^rcmrow([a-z0-9\-_=\+\/]+)/i;
this.rows = {};
this.selection = [];
this.rowcount = 0;
this.colcount = 0;
this.subject_col = 0;
this.modkey = 0;
this.multiselect = false;
this.multiexpand = false;
this.multi_selecting = false;
this.draggable = false;
this.column_movable = false;
this.keyboard = false;
this.toggleselect = false;
this.aria_listbox = false;
this.parent_focus = true;
this.checkbox_selection = false;
this.drag_active = false;
this.col_drag_active = false;
this.column_fixed = null;
this.last_selected = null;
this.shift_start = null;
this.focused = false;
this.drag_mouse_start = null;
this.dblclick_time = 500; // default value on MS Windows is 500
this.row_init = function(){}; // @deprecated; use list.addEventListener('initrow') instead
this.touch_start_time = 0; // start time of the touch event
this.touch_event_time = 500; // maximum time a touch should be considered a left mouse button event, after this its something else (eg contextmenu event)
// overwrite default paramaters
if (p && typeof p === 'object')
for (var n in p)
this[n] = p[n];
// register this instance
rcube_list_widget._instances.push(this);
};
rcube_list_widget.prototype = {
/**
* get all message rows from HTML table and init each row
*/
init: function()
{
if (this.tagname == 'table' && this.list && this.list.tBodies[0]) {
this.thead = this.list.tHead;
this.tbody = this.list.tBodies[0];
}
else if (this.tagname != 'table' && this.list) {
this.tbody = this.list;
}
if ($(this.list).attr('role') == 'listbox') {
this.aria_listbox = true;
if (this.multiselect)
$(this.list).attr('aria-multiselectable', 'true');
}
var me = this;
if (this.tbody) {
this.rows = {};
this.rowcount = 0;
var r, len, rows = this.tbody.childNodes;
for (r=0, len=rows.length; r<len; r++) {
if (rows[r].nodeType == 1)
this.rowcount += this.init_row(rows[r]) ? 1 : 0;
}
this.init_header();
this.frame = this.list.parentNode;
// set body events
if (this.keyboard) {
rcube_event.add_listener({event:'keydown', object:this, method:'key_press'});
// allow the table element to receive focus.
$(this.list).attr('tabindex', '0')
.on('focus', function(e) { me.focus(e); });
}
}
if (this.parent_focus) {
this.list.parentNode.onclick = function(e) { me.focus(); };
}
rcmail.triggerEvent('initlist', { obj: this.list });
return this;
},
/**
* Init list row and set mouse events on it
*/
init_row: function(row)
{
row.uid = this.get_row_uid(row);
// make references in internal array and set event handlers
if (row && row.uid) {
var self = this, uid = row.uid;
this.rows[uid] = {uid:uid, id:row.id, obj:row};
$(row).data('uid', uid)
// set eventhandlers to table row (only left-button-clicks in mouseup)
.mousedown(function(e) { return self.drag_row(e, this.uid); })
.mouseup(function(e) {
if (e.which == 1 && !self.drag_active && !$(e.currentTarget).is('.ui-droppable-active'))
return self.click_row(e, this.uid);
else
return true;
});
// for IE and Edge differentiate between touch, touch+hold using pointer events rather than touch
if ((bw.ie || bw.edge) && bw.pointer) {
$(row).on('pointerdown', function(e) {
if (e.pointerType == 'touch') {
self.touch_start_time = new Date().getTime();
return false;
}
})
.on('pointerup', function(e) {
if (e.pointerType == 'touch') {
var duration = (new Date().getTime() - self.touch_start_time);
if (duration <= self.touch_event_time) {
self.drag_row(e, this.uid);
return self.click_row(e, this.uid);
}
}
});
}
else if (bw.touch && row.addEventListener) {
row.addEventListener('touchstart', function(e) {
if (e.touches.length == 1) {
self.touchmoved = false;
self.drag_row(rcube_event.touchevent(e.touches[0]), this.uid);
self.touch_start_time = new Date().getTime();
}
}, false);
row.addEventListener('touchend', function(e) {
if (e.changedTouches.length == 1) {
var duration = (new Date().getTime() - self.touch_start_time);
if (!self.touchmoved && duration <= self.touch_event_time && !self.click_row(rcube_event.touchevent(e.changedTouches[0]), this.uid))
e.preventDefault();
}
}, false);
row.addEventListener('touchmove', function(e) {
if (e.changedTouches.length == 1) {
self.touchmoved = true;
if (self.drag_active)
e.preventDefault();
}
}, false);
}
// label the list row with the subject col as descriptive label
if (this.aria_listbox) {
var lbl_id = 'l:' + row.id;
$(row)
.attr('role', 'option')
.attr('aria-labelledby', lbl_id)
.find(this.col_tagname()).eq(this.subject_column()).attr('id', lbl_id);
}
if (document.all)
row.onselectstart = function() { return false; };
this.row_init(this.rows[uid]); // legacy support
this.triggerEvent('initrow', this.rows[uid]);
return true;
}
},
/**
* Init list column headers and set mouse events on them
*/
init_header: function()
{
if (this.thead) {
this.colcount = 0;
if (this.fixed_header) { // copy (modified) fixed header back to the actual table
$(this.list.tHead).replaceWith($(this.fixed_header).find('thead').clone());
$(this.list.tHead).find('th,td').attr('style', '').find('a').attr('tabindex', '-1'); // remove fixed widths
}
else if (!bw.touch && this.list.className.indexOf('fixedheader') >= 0) {
this.init_fixed_header();
}
var col, r, p = this;
// add events for list columns moving
if (this.column_movable && this.thead && this.thead.rows) {
for (r=0; r<this.thead.rows[0].cells.length; r++) {
if (this.column_fixed == r)
continue;
col = this.thead.rows[0].cells[r];
col.onmousedown = function(e) { return p.drag_column(e, this); };
this.colcount++;
}
}
}
},
init_fixed_header: function()
{
var clone = $(this.list.tHead).clone();
if (!this.fixed_header) {
this.fixed_header = $('<table>')
.attr('class', this.list.className + ' fixedcopy')
.attr('role', 'presentation')
.css({ position:'fixed' })
.append(clone)
.append('<tbody></tbody>');
$(this.list).before(this.fixed_header);
var me = this;
$(window).resize(function() { me.resize(); });
$(window).scroll(function() {
var w = $(window);
me.fixed_header.css({
marginLeft: -w.scrollLeft() + 'px',
marginTop: -w.scrollTop() + 'px'
});
});
}
else {
$(this.fixed_header).find('thead').replaceWith(clone);
}
// avoid scrolling header links being focused
$(this.list.tHead).find('a.sortcol').attr('tabindex', '-1');
// set tabindex to fixed header sort links
clone.find('a.sortcol').attr('tabindex', '0');
this.thead = clone.get(0);
this.resize();
},
resize: function()
{
if (!this.fixed_header)
return;
var column_widths = [];
// get column widths from original thead
$(this.tbody).parent().find('thead th,thead td').each(function(index) {
column_widths[index] = $(this).width();
});
// apply fixed widths to fixed table header
$(this.thead).parent().width($(this.tbody).parent().width());
$(this.thead).find('th,td').each(function(index) {
$(this).width(column_widths[index]);
});
$(window).scroll();
},
/**
* Remove all list rows
*/
clear: function(sel)
{
if (this.tagname == 'table') {
var tbody = document.createElement('tbody');
this.list.insertBefore(tbody, this.tbody);
this.list.removeChild(this.list.tBodies[1]);
this.tbody = tbody;
}
else {
$(this.row_tagname() + ':not(.thead)', this.tbody).remove();
}
this.rows = {};
this.rowcount = 0;
this.last_selected = null;
if (sel)
this.clear_selection();
// reset scroll position (in Opera)
if (this.frame)
this.frame.scrollTop = 0;
// fix list header after removing any rows
this.resize();
},
/**
* 'remove' message row from list (just hide it)
*/
remove_row: function(uid, sel_next)
{
var self = this, node = this.rows[uid] ? this.rows[uid].obj : null;
if (!node)
return;
node.style.display = 'none';
if (sel_next)
this.select_next();
delete this.rows[uid];
this.rowcount--;
// fix list header after removing any rows
clearTimeout(this.resize_timeout)
this.resize_timeout = setTimeout(function() { self.resize(); }, 50);
},
/**
* Add row to the list and initialize it
*/
insert_row: function(row, before)
{
var self = this, tbody = this.tbody;
// create a real dom node first
if (row.nodeName === undefined) {
// for performance reasons use DOM instead of jQuery here
var i, e, domcell, col,
domrow = document.createElement(this.row_tagname());
if (row.id) domrow.id = row.id;
if (row.uid) domrow.uid = row.uid;
if (row.className) domrow.className = row.className;
if (row.style) $.extend(domrow.style, row.style);
for (i=0; row.cols && i < row.cols.length; i++) {
col = row.cols[i];
domcell = col.dom;
if (!domcell) {
domcell = document.createElement(this.col_tagname());
if (col.className) domcell.className = col.className;
if (col.innerHTML) domcell.innerHTML = col.innerHTML;
for (e in col.events)
domcell['on' + e] = col.events[e];
}
domrow.appendChild(domcell);
}
row = domrow;
}
if (this.checkbox_selection) {
- var key, cell = document.createElement(this.col_tagname()),
- chbox = document.createElement('input');
-
- chbox.type = 'checkbox';
- chbox.tabIndex = -1;
- chbox.onchange = function(e) {
- self.select_row(row.uid, key || CONTROL_KEY, true);
- e.stopPropagation();
- key = null;
- };
- chbox.onmousedown = function(e) {
- key = rcube_event.get_modifier(e);
- };
-
- cell.className = 'selection';
- // make the whole cell "touchable" for touch devices
- cell.onclick = function(e) {
- if (!$(e.target).is('input')) {
- key = rcube_event.get_modifier(e);
- $(chbox).prop('checked', !chbox.checked).change();
- }
-
- e.stopPropagation();
- };
-
- cell.appendChild(chbox);
-
- row.insertBefore(cell, row.firstChild);
+ this.insert_checkbox(row);
}
if (before && tbody.childNodes.length)
tbody.insertBefore(row, (typeof before == 'object' && before.parentNode == tbody) ? before : tbody.firstChild);
else
tbody.appendChild(row);
this.init_row(row);
this.rowcount++;
// fix list header after adding any rows
clearTimeout(this.resize_timeout)
this.resize_timeout = setTimeout(function() { self.resize(); }, 50);
},
/**
- *
+ * Update existing record
*/
update_row: function(id, cols, newid, select)
{
var row = this.rows[id];
if (!row) return false;
var i, domrow = row.obj;
for (i = 0; cols && i < cols.length; i++) {
this.get_cell(domrow, i).html(cols[i]);
}
if (newid) {
delete this.rows[id];
domrow.uid = newid;
domrow.id = 'rcmrow' + newid;
this.init_row(domrow);
if (select)
this.selection[0] = newid;
if (this.last_selected == id)
this.last_selected = newid;
}
},
+/**
+ * Add selection checkbox to the list record
+ */
+insert_checkbox: function(row)
+{
+ var key, self = this,
+ cell = document.createElement(this.col_tagname()),
+ chbox = document.createElement('input');
+
+ chbox.type = 'checkbox';
+ chbox.tabIndex = -1;
+ chbox.onchange = function(e) {
+ self.select_row(row.uid, key || CONTROL_KEY, true);
+ e.stopPropagation();
+ key = null;
+ };
+ chbox.onmousedown = function(e) {
+ key = rcube_event.get_modifier(e);
+ };
+
+ cell.className = 'selection';
+ // make the whole cell "touchable" for touch devices
+ cell.onclick = function(e) {
+ if (!$(e.target).is('input')) {
+ key = rcube_event.get_modifier(e);
+ $(chbox).prop('checked', !chbox.checked).change();
+ }
+ e.stopPropagation();
+ };
+
+ cell.appendChild(chbox);
+
+ row.insertBefore(cell, row.firstChild);
+},
+
+/**
+ * Enable checkbox selection
+ */
+enable_checkbox_selection: function()
+{
+ this.checkbox_selection = true;
+
+ // Add checkbox to existing records if any
+ var r, len, cell, row_tag = this.row_tagname().toUpperCase(),
+ rows = this.tbody.childNodes;
+
+ for (r=0, len=rows.length; r<len; r++) {
+ if (rows[r].nodeName == row_tag && (cell = rows[r].firstChild)) {
+ if (cell.className == 'selection')
+ break;
+ this.insert_checkbox(rows[r]);
+ }
+ }
+},
/**
* Set focus to the list
*/
focus: function(e)
{
if (this.focused)
return;
this.focused = true;
if (e)
rcube_event.cancel(e);
var focus_elem = null;
if (this.last_selected && this.rows[this.last_selected]) {
focus_elem = $(this.rows[this.last_selected].obj).find(this.col_tagname()).eq(this.subject_column()).attr('tabindex', '0');
}
// Un-focus already focused elements (#1487123, #1487316, #1488600, #1488620)
if (focus_elem && focus_elem.length) {
// We now fix this by explicitly assigning focus to a dedicated link element
this.focus_noscroll(focus_elem);
}
else {
// It looks that window.focus() does the job for all browsers, but not Firefox (#1489058)
$('iframe,:focus:not(body)').blur();
window.focus();
}
$(this.list).addClass('focus').removeAttr('tabindex');
// set internal focus pointer to first row
if (!this.last_selected)
this.select_first(CONTROL_KEY);
},
/**
* remove focus from the list
*/
blur: function(e)
{
this.focused = false;
// avoid the table getting focus right again (on Shift+Tab)
var me = this;
setTimeout(function() { $(me.list).attr('tabindex', '0'); }, 20);
if (this.last_selected && this.rows[this.last_selected]) {
$(this.rows[this.last_selected].obj)
.find(this.col_tagname()).eq(this.subject_column()).removeAttr('tabindex');
}
$(this.list).removeClass('focus');
},
/**
* Focus the given element without scrolling the list container
*/
focus_noscroll: function(elem)
{
var y = this.frame.scrollTop || this.frame.scrollY;
elem.focus();
this.frame.scrollTop = y;
},
/**
* Set/unset the given column as hidden
*/
hide_column: function(col, hide)
{
var method = hide ? 'addClass' : 'removeClass';
if (this.fixed_header)
$(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.fixed_header)[method]('hidden');
$(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.list)[method]('hidden');
},
/**
* onmousedown-handler of message list column
*/
drag_column: function(e, col)
{
if (this.colcount > 1) {
this.drag_start = true;
this.drag_mouse_start = rcube_event.get_mouse_pos(e);
rcube_event.add_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
rcube_event.add_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
// enable dragging over iframes
this.add_dragfix();
// find selected column number
for (var i=0; i<this.thead.rows[0].cells.length; i++) {
if (col == this.thead.rows[0].cells[i]) {
this.selected_column = i;
break;
}
}
}
return false;
},
/**
* onmousedown-handler of message list row
*/
drag_row: function(e, id)
{
// don't do anything (another action processed before)
if (!this.is_event_target(e))
return true;
// accept right-clicks
if (rcube_event.get_button(e) == 2)
return true;
this.in_selection_before = e && e.istouch || this.in_selection(id) ? id : false;
// selects currently unselected row
if (!this.in_selection_before) {
var mod_key = rcube_event.get_modifier(e);
this.select_row(id, mod_key, true);
}
if (this.draggable && this.selection.length && this.in_selection(id)) {
this.drag_start = true;
this.drag_mouse_start = rcube_event.get_mouse_pos(e);
rcube_event.add_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
rcube_event.add_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
if (bw.touch) {
rcube_event.add_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
rcube_event.add_listener({event:'touchend', object:this, method:'drag_mouse_up'});
}
// enable dragging over iframes
this.add_dragfix();
}
return false;
},
/**
* onmouseup-handler of message list row
*/
click_row: function(e, id)
{
// sanity check
if (!id || !this.rows[id])
return false;
// don't do anything (another action processed before)
if (!this.is_event_target(e))
return true;
var now = new Date().getTime(),
dblclicked = now - this.rows[id].clicked < this.dblclick_time;
// unselects currently selected row
if (!this.drag_active && !dblclicked && this.in_selection_before == id)
this.select_row(id, rcube_event.get_modifier(e), true);
this.drag_start = false;
this.in_selection_before = false;
// row was double clicked
if (this.rowcount && dblclicked && this.in_selection(id)) {
this.triggerEvent('dblclick');
now = 0;
}
else
this.triggerEvent('click');
if (!this.drag_active) {
// remove temp divs
this.del_dragfix();
rcube_event.cancel(e);
}
this.rows[id].clicked = now;
this.focus();
return false;
},
/**
* Check target of the current event
*/
is_event_target: function(e)
{
var target = rcube_event.get_target(e),
tagname = target.tagName.toLowerCase();
return !(target && (tagname == 'input' || tagname == 'img' || (tagname != 'a' && target.onclick) || $(target).data('action-link')));
},
/*
* Returns thread root ID for specified row ID
*/
find_root: function(uid)
{
var r = this.rows[uid];
if (r && r.parent_uid)
return this.find_root(r.parent_uid);
else
return uid;
},
expand_row: function(e, id)
{
var row = this.rows[id],
evtarget = rcube_event.get_target(e),
mod_key = rcube_event.get_modifier(e);
// Don't treat double click on the expando as double click on the message.
row.clicked = 0;
if (row.expanded) {
evtarget.className = 'collapsed';
if (mod_key == CONTROL_KEY || this.multiexpand)
this.collapse_all(row);
else
this.collapse(row);
}
else {
evtarget.className = 'expanded';
if (mod_key == CONTROL_KEY || this.multiexpand)
this.expand_all(row);
else
this.expand(row);
}
},
collapse: function(row)
{
var r, depth = row.depth,
new_row = row ? row.obj.nextSibling : null;
row.expanded = false;
this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
while (new_row) {
if (new_row.nodeType == 1) {
r = this.rows[new_row.uid];
if (r && r.depth <= depth)
break;
$(new_row).css('display', 'none');
if (r.expanded) {
r.expanded = false;
this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
}
}
new_row = new_row.nextSibling;
}
this.resize();
this.triggerEvent('listupdate');
return false;
},
expand: function(row)
{
var r, p, depth, new_row, last_expanded_parent_depth;
if (row) {
row.expanded = true;
depth = row.depth;
new_row = row.obj.nextSibling;
this.update_expando(row.id, true);
this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
}
else {
var tbody = this.tbody;
new_row = tbody.firstChild;
depth = 0;
last_expanded_parent_depth = 0;
}
while (new_row) {
if (new_row.nodeType == 1) {
r = this.rows[new_row.uid];
if (r) {
if (row && (!r.depth || r.depth <= depth))
break;
if (r.parent_uid) {
p = this.rows[r.parent_uid];
if (p && p.expanded) {
if ((row && p == row) || last_expanded_parent_depth >= p.depth - 1) {
last_expanded_parent_depth = p.depth;
$(new_row).css('display', '');
r.expanded = true;
this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
}
}
else
if (row && (! p || p.depth <= depth))
break;
}
}
}
new_row = new_row.nextSibling;
}
this.resize();
this.triggerEvent('listupdate');
return false;
},
collapse_all: function(row)
{
var depth, new_row, r;
if (row) {
row.expanded = false;
depth = row.depth;
new_row = row.obj.nextSibling;
this.update_expando(row.id);
this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
// don't collapse sub-root tree in multiexpand mode
if (depth && this.multiexpand)
return false;
}
else {
new_row = this.tbody.firstChild;
depth = 0;
}
while (new_row) {
if (new_row.nodeType == 1) {
if (r = this.rows[new_row.uid]) {
if (row && (!r.depth || r.depth <= depth))
break;
if (row || r.depth)
$(new_row).css('display', 'none');
if (r.has_children && r.expanded) {
r.expanded = false;
this.update_expando(r.id, false);
this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
}
}
}
new_row = new_row.nextSibling;
}
this.resize();
this.triggerEvent('listupdate');
return false;
},
expand_all: function(row)
{
var depth, new_row, r;
if (row) {
row.expanded = true;
depth = row.depth;
new_row = row.obj.nextSibling;
this.update_expando(row.id, true);
this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
}
else {
new_row = this.tbody.firstChild;
depth = 0;
}
while (new_row) {
if (new_row.nodeType == 1) {
if (r = this.rows[new_row.uid]) {
if (row && r.depth <= depth)
break;
$(new_row).css('display', '');
if (r.has_children && !r.expanded) {
r.expanded = true;
this.update_expando(r.id, true);
this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
}
}
}
new_row = new_row.nextSibling;
}
this.resize();
this.triggerEvent('listupdate');
return false;
},
update_expando: function(id, expanded)
{
var expando = document.getElementById('rcmexpando' + id);
if (expando)
expando.className = expanded ? 'expanded' : 'collapsed';
},
get_row_uid: function(row)
{
if (!row)
return;
if (!row.uid) {
var uid = $(row).data('uid');
if (uid)
row.uid = uid;
else if (String(row.id).match(this.id_regexp))
row.uid = RegExp.$1;
}
return row.uid;
},
/**
* get first/next/previous/last rows that are not hidden
*/
get_next_row: function()
{
if (!this.rowcount)
return false;
var last_selected_row = this.rows[this.last_selected],
new_row = last_selected_row ? last_selected_row.obj.nextSibling : null;
while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
new_row = new_row.nextSibling;
return new_row;
},
get_prev_row: function()
{
if (!this.rowcount)
return false;
var last_selected_row = this.rows[this.last_selected],
new_row = last_selected_row ? last_selected_row.obj.previousSibling : null;
while (new_row && (new_row.nodeType != 1 || new_row.style.display == 'none'))
new_row = new_row.previousSibling;
return new_row;
},
get_first_row: function()
{
if (this.rowcount) {
var i, uid, rows = this.tbody.childNodes;
for (i=0; i<rows.length; i++)
if (rows[i].id && (uid = this.get_row_uid(rows[i])) && this.rows[uid])
return uid;
}
return null;
},
get_last_row: function()
{
if (this.rowcount) {
var i, uid, rows = this.tbody.childNodes;
for (i=rows.length-1; i>=0; i--)
if (rows[i].id && (uid = this.get_row_uid(rows[i])) && this.rows[uid])
return uid;
}
return null;
},
get_next: function()
{
var row;
if (row = this.get_next_row()) {
return row.uid;
}
},
get_prev: function()
{
var row;
if (row = this.get_prev_row()) {
return row.uid;
}
},
row_tagname: function()
{
var row_tagnames = { table:'tr', ul:'li', '*':'div' };
return row_tagnames[this.tagname] || row_tagnames['*'];
},
col_tagname: function()
{
var col_tagnames = { table:'td', '*':'span' };
return col_tagnames[this.tagname] || col_tagnames['*'];
},
get_cell: function(row, index)
{
return $(this.col_tagname(), row).eq(index + (this.checkbox_selection ? 1 : 0));
},
/**
* selects or unselects the proper row depending on the modifier key pressed
*/
select_row: function(id, mod_key, with_mouse)
{
var select_before = this.selection.join(','),
in_selection_before = this.in_selection(id);
if (!this.multiselect && with_mouse)
mod_key = 0;
if (!this.shift_start)
this.shift_start = id
if (!mod_key) {
this.shift_start = id;
this.highlight_row(id, false);
this.multi_selecting = false;
}
else {
switch (mod_key) {
case SHIFT_KEY:
this.shift_select(id, false);
break;
case CONTROL_KEY:
if (with_mouse) {
this.shift_start = id;
this.highlight_row(id, true);
}
break;
case CONTROL_SHIFT_KEY:
this.shift_select(id, true);
break;
default:
this.highlight_row(id, false);
break;
}
this.multi_selecting = true;
}
if (this.last_selected && this.rows[this.last_selected]) {
$(this.rows[this.last_selected].obj).removeClass('focused')
.find(this.col_tagname()).eq(this.subject_column()).removeAttr('tabindex');
}
// unselect if toggleselect is active and the same row was clicked again
if (this.toggleselect && in_selection_before && !mod_key) {
this.clear_selection();
}
// trigger event if selection changed
else if (this.selection.join(',') != select_before) {
this.triggerEvent('select');
}
if (this.rows[id]) {
$(this.rows[id].obj).addClass('focused');
// set cursor focus to link inside selected row
if (this.focused)
this.focus_noscroll($(this.rows[id].obj).find(this.col_tagname()).eq(this.subject_column()).attr('tabindex', '0'));
}
if (!this.selection.length)
this.shift_start = null;
this.last_selected = id;
},
/**
* Alias method for select_row
*/
select: function(id)
{
this.select_row(id, false);
this.scrollto(id);
},
/**
* Select row next to the last selected one.
* Either below or above.
*/
select_next: function()
{
var new_row = this.get_next_row() || this.get_prev_row();
if (new_row)
this.select_row(new_row.uid, false, false);
},
/**
* Select first row
*/
select_first: function(mod_key)
{
var row = this.get_first_row();
if (row) {
this.select_row(row, mod_key, false);
this.scrollto(row);
}
},
/**
* Select last row
*/
select_last: function(mod_key)
{
var row = this.get_last_row();
if (row) {
this.select_row(row, mod_key, false);
this.scrollto(row);
}
},
/**
* Add all childs of the given row to selection
*/
select_children: function(uid)
{
var i, children = this.row_children(uid), len = children.length;
for (i=0; i<len; i++)
if (!this.in_selection(children[i]))
this.select_row(children[i], CONTROL_KEY, true);
},
/**
* Perform selection when shift key is pressed
*/
shift_select: function(id, control)
{
if (!this.rows[this.shift_start] || !this.selection.length)
this.shift_start = id;
var n, i, j, to_row = this.rows[id],
from_rowIndex = this._rowIndex(this.rows[this.shift_start].obj),
to_rowIndex = this._rowIndex(to_row.obj);
// if we're going down the list, and we hit a thread, and it's closed, select the whole thread
if (from_rowIndex < to_rowIndex && !to_row.expanded && to_row.has_children)
if (to_row = this.rows[(this.row_children(id)).pop()])
to_rowIndex = this._rowIndex(to_row.obj);
i = ((from_rowIndex < to_rowIndex) ? from_rowIndex : to_rowIndex),
j = ((from_rowIndex > to_rowIndex) ? from_rowIndex : to_rowIndex);
// iterate through the entire message list
for (n in this.rows) {
if (this._rowIndex(this.rows[n].obj) >= i && this._rowIndex(this.rows[n].obj) <= j) {
if (!this.in_selection(n)) {
this.highlight_row(n, true);
}
}
else {
if (this.in_selection(n) && !control) {
this.highlight_row(n, true);
}
}
}
},
/**
* Helper method to emulate the rowIndex property of non-tr elements
*/
_rowIndex: function(obj)
{
return (obj.rowIndex !== undefined) ? obj.rowIndex : $(obj).prevAll().length;
},
/**
* Check if given id is part of the current selection
*/
in_selection: function(id, index)
{
for (var n in this.selection)
if (this.selection[n] == id)
return index ? parseInt(n) : true;
return false;
},
/**
* Select each row in list
*/
select_all: function(filter)
{
if (!this.rowcount)
return false;
// reset but remember selection first
var n, select_before = this.selection.join(',');
this.selection = [];
for (n in this.rows) {
if (!filter || this.rows[n][filter] == true) {
this.last_selected = n;
this.highlight_row(n, true, true);
}
else {
$(this.rows[n].obj).removeClass('selected').removeAttr('aria-selected');
}
}
// trigger event if selection changed
if (this.selection.join(',') != select_before)
this.triggerEvent('select');
this.focus();
return true;
},
/**
* Invert selection
*/
invert_selection: function()
{
if (!this.rowcount)
return false;
// remember old selection
var n, select_before = this.selection.join(',');
for (n in this.rows)
this.highlight_row(n, true);
// trigger event if selection changed
if (this.selection.join(',') != select_before)
this.triggerEvent('select');
this.focus();
return true;
},
/**
* Unselect selected row(s)
*/
clear_selection: function(id, no_event)
{
var n, num_select = this.selection.length;
// one row
if (id) {
for (n in this.selection)
if (this.selection[n] == id) {
this.selection.splice(n,1);
break;
}
}
// all rows
else {
for (n in this.selection)
if (this.rows[this.selection[n]]) {
$(this.rows[this.selection[n]].obj).removeClass('selected').removeAttr('aria-selected');
}
this.selection = [];
}
if (this.checkbox_selection)
$(this.row_tagname() + ':not(.selected) > .selection > input:checked', this.list).prop('checked', false);
if (num_select && !this.selection.length && !no_event) {
this.triggerEvent('select');
this.last_selected = null;
}
},
/**
* Getter for the selection array
*/
get_selection: function(deep)
{
var res = $.merge([], this.selection);
var props = {deep: deep, res: res};
if (this.triggerEvent('getselection', props) === false)
return props.res;
// return children of selected threads even if only root is selected
if (deep !== false && res.length) {
for (var uid, uids, i=0, len=res.length; i<len; i++) {
uid = res[i];
if (this.rows[uid] && this.rows[uid].has_children && !this.rows[uid].expanded) {
uids = this.row_children(uid);
for (var j=0, uids_len=uids.length; j<uids_len; j++) {
uid = uids[j];
if (!this.in_selection(uid))
res.push(uid);
}
}
}
}
return res;
},
/**
* Return the ID if only one row is selected
*/
get_single_selection: function()
{
var selection = this.get_selection(false);
if (selection.length == 1)
return selection[0];
else
return null;
},
/**
* Highlight/unhighlight a row
*/
highlight_row: function(id, multiple, norecur)
{
if (!this.rows[id])
return;
if (!multiple) {
if (this.selection.length > 1 || !this.in_selection(id)) {
this.clear_selection(null, true);
this.selection[0] = id;
$(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');
if (this.checkbox_selection)
$('.selection > input', this.rows[id].obj).prop('checked', true);
}
}
else {
var pre, post, p = this.in_selection(id, true);
if (p === false) { // select row
this.selection.push(id);
$(this.rows[id].obj).addClass('selected').attr('aria-selected', 'true');
if (this.checkbox_selection)
$('.selection > input', this.rows[id].obj).prop('checked', true);
if (!norecur && !this.rows[id].expanded)
this.highlight_children(id, true);
}
else { // unselect row
pre = this.selection.slice(0, p);
post = this.selection.slice(p+1, this.selection.length);
this.selection = pre.concat(post);
$(this.rows[id].obj).removeClass('selected').removeAttr('aria-selected');
if (this.checkbox_selection)
$('.selection > input', this.rows[id].obj).prop('checked', false);
if (!norecur && !this.rows[id].expanded)
this.highlight_children(id, false);
}
}
},
/**
* Highlight/unhighlight all childs of the given row
*/
highlight_children: function(id, status)
{
var i, selected,
children = this.row_children(id), len = children.length;
for (i=0; i<len; i++) {
selected = this.in_selection(children[i]);
if ((status && !selected) || (!status && selected))
this.highlight_row(children[i], true, true);
}
},
/**
* Handler for keyboard events
*/
key_press: function(e)
{
var target = e.target || {};
if (!this.focused || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')
return true;
var keyCode = rcube_event.get_keycode(e),
mod_key = rcube_event.get_modifier(e);
switch (keyCode) {
case 40:
case 38:
case 63233: // "down", in safari keypress
case 63232: // "up", in safari keypress
// Stop propagation so that the browser doesn't scroll
rcube_event.cancel(e);
return this.use_arrow_key(keyCode, mod_key);
case 32:
rcube_event.cancel(e);
return this.select_row(this.last_selected, mod_key, true);
case 37: // Left arrow key
case 39: // Right arrow key
// Stop propagation
rcube_event.cancel(e);
var ret = this.use_arrow_key(keyCode, mod_key);
this.key_pressed = keyCode;
this.modkey = mod_key;
this.triggerEvent('keypress');
this.modkey = 0;
return ret;
case 36: // Home
this.select_first(mod_key);
return rcube_event.cancel(e);
case 35: // End
this.select_last(mod_key);
return rcube_event.cancel(e);
case 27:
if (this.drag_active)
return this.drag_mouse_up(e);
if (this.col_drag_active) {
this.selected_column = null;
return this.column_drag_mouse_up(e);
}
return rcube_event.cancel(e);
case 9: // Tab
this.blur();
break;
case 13: // Enter
if (!this.selection.length)
this.select_row(this.last_selected, mod_key, false);
default:
this.key_pressed = keyCode;
this.modkey = mod_key;
this.triggerEvent('keypress');
this.modkey = 0;
if (this.key_pressed == this.BACKSPACE_KEY)
return rcube_event.cancel(e);
}
return true;
},
/**
* Special handling method for arrow keys
*/
use_arrow_key: function(keyCode, mod_key)
{
var new_row,
selected_row = this.rows[this.last_selected];
// Safari uses the nonstandard keycodes 63232/63233 for up/down, if we're
// using the keypress event (but not the keydown or keyup event).
if (keyCode == 40 || keyCode == 63233) // down arrow key pressed
new_row = this.get_next_row();
else if (keyCode == 38 || keyCode == 63232) // up arrow key pressed
new_row = this.get_prev_row();
else {
if (!selected_row || !selected_row.has_children)
return;
// expand
if (keyCode == 39) {
if (selected_row.expanded)
return;
if (mod_key == CONTROL_KEY || this.multiexpand)
this.expand_all(selected_row);
else
this.expand(selected_row);
}
// collapse
else {
if (!selected_row.expanded)
return;
if (mod_key == CONTROL_KEY || this.multiexpand)
this.collapse_all(selected_row);
else
this.collapse(selected_row);
}
this.update_expando(selected_row.id, selected_row.expanded);
return false;
}
if (new_row) {
// simulate ctr-key if no rows are selected
if (!mod_key && !this.selection.length)
mod_key = CONTROL_KEY;
this.select_row(new_row.uid, mod_key, false);
this.scrollto(new_row.uid);
}
else if (!new_row && !selected_row) {
// select the first row if none selected yet
this.select_first(CONTROL_KEY);
}
return false;
},
/**
* Try to scroll the list to make the specified row visible
*/
scrollto: function(id)
{
var row = this.rows[id] ? this.rows[id].obj : null;
if (row && this.frame) {
var scroll_to = Number(row.offsetTop),
head_offset = 0;
// expand thread if target row is hidden (collapsed)
if (!scroll_to && this.rows[id].parent_uid) {
var parent = this.find_root(this.rows[id].uid);
this.expand_all(this.rows[parent]);
scroll_to = Number(row.offsetTop);
}
if (this.fixed_header)
head_offset = Number(this.thead.offsetHeight);
// if row is above the frame (or behind header)
if (scroll_to < Number(this.frame.scrollTop) + head_offset) {
// scroll window so that row isn't behind header
this.frame.scrollTop = scroll_to - head_offset;
}
else if (scroll_to + Number(row.offsetHeight) > Number(this.frame.scrollTop) + Number(this.frame.offsetHeight))
this.frame.scrollTop = (scroll_to + Number(row.offsetHeight)) - Number(this.frame.offsetHeight);
}
},
/**
* Handler for mouse move events
*/
drag_mouse_move: function(e)
{
// convert touch event
if (e.type == 'touchmove') {
if (e.touches.length == 1 && e.changedTouches.length == 1)
e = rcube_event.touchevent(e.changedTouches[0]);
else
return rcube_event.cancel(e);
}
if (this.drag_start) {
// check mouse movement, of less than 3 pixels, don't start dragging
var m = rcube_event.get_mouse_pos(e),
limit = 10, selection = [], self = this;
if (!this.drag_mouse_start || (Math.abs(m.x - this.drag_mouse_start.x) < 3 && Math.abs(m.y - this.drag_mouse_start.y) < 3))
return false;
// remember dragging start position
this.drag_start_pos = {left: m.x, top: m.y};
// initialize drag layer
if (!this.draglayer)
this.draglayer = $('<div>').attr('id', 'rcmdraglayer')
.css({position: 'absolute', display: 'none', 'z-index': 2000})
.appendTo(document.body);
else
this.draglayer.html('');
// get selected rows (in display order), don't use this.selection here
$(this.row_tagname() + '.selected', this.tbody).each(function() {
var uid = self.get_row_uid(this), row = self.rows[uid];
if (!row || $.inArray(uid, selection) > -1)
return;
selection.push(uid);
// also handle children of (collapsed) trees for dragging (they might be not selected)
if (row.has_children && !row.expanded)
$.each(self.row_children(uid), function() {
if ($.inArray(this, selection) > -1)
return;
selection.push(this);
});
// break the loop asap
if (selection.length > limit + 1)
return false;
});
// append subject (of every row up to the limit) to the drag layer
$.each(selection, function(i, uid) {
if (i > limit) {
self.draglayer.append($('<div>').text('...'));
return false;
}
var subject_col = self.subject_column();
$('> ' + self.col_tagname(), self.rows[uid].obj).each(function(n, cell) {
if (subject_col < 0 || (subject_col >= 0 && subject_col == n)) {
// remove elements marked with "skip-on-drag" class
cell = $(cell).clone();
$(cell).find('.skip-on-drag').remove();
var subject = cell.text();
if (subject) {
// remove leading spaces
subject = $.trim(subject);
// truncate line to 50 characters
subject = (subject.length > 50 ? subject.substring(0, 50) + '...' : subject);
self.draglayer.append($('<div>').text(subject));
return false;
}
}
});
});
this.draglayer.show();
this.drag_active = true;
this.triggerEvent('dragstart');
}
if (this.drag_active && this.draglayer) {
var pos = rcube_event.get_mouse_pos(e);
this.draglayer.css({ left:(pos.x+20)+'px', top:(pos.y-5 + (bw.ie ? document.documentElement.scrollTop : 0))+'px' });
this.triggerEvent('dragmove', e?e:window.event);
}
this.drag_start = false;
return false;
},
/**
* Handler for mouse up events
*/
drag_mouse_up: function(e)
{
document.onmousemove = null;
if (e.type == 'touchend') {
if (e.changedTouches.length != 1)
return rcube_event.cancel(e);
}
if (this.draglayer && this.draglayer.is(':visible')) {
if (this.drag_start_pos)
this.draglayer.animate(this.drag_start_pos, 300, 'swing').hide(20);
else
this.draglayer.hide();
}
if (this.drag_active)
this.focus();
this.drag_active = false;
rcube_event.remove_listener({event:'mousemove', object:this, method:'drag_mouse_move'});
rcube_event.remove_listener({event:'mouseup', object:this, method:'drag_mouse_up'});
if (bw.touch) {
rcube_event.remove_listener({event:'touchmove', object:this, method:'drag_mouse_move'});
rcube_event.remove_listener({event:'touchend', object:this, method:'drag_mouse_up'});
}
// remove temp divs
this.del_dragfix();
this.triggerEvent('dragend', e);
return rcube_event.cancel(e);
},
/**
* Handler for mouse move events for dragging list column
*/
column_drag_mouse_move: function(e)
{
if (this.drag_start) {
// check mouse movement, of less than 3 pixels, don't start dragging
var i, m = rcube_event.get_mouse_pos(e);
if (!this.drag_mouse_start || (Math.abs(m.x - this.drag_mouse_start.x) < 3 && Math.abs(m.y - this.drag_mouse_start.y) < 3))
return false;
if (!this.col_draglayer) {
var lpos = $(this.list).offset(),
cells = this.thead.rows[0].cells;
// fix layer position when list is scrolled
lpos.top += this.list.scrollTop + this.list.parentNode.scrollTop;
// create dragging layer
this.col_draglayer = $('<div>').attr('id', 'rcmcoldraglayer')
.css(lpos).css({ position:'absolute', 'z-index':2001,
'background-color':'white', opacity:0.75,
height: (this.frame.offsetHeight-2)+'px', width: (this.frame.offsetWidth-2)+'px' })
.appendTo(document.body)
// ... and column position indicator
.append($('<div>').attr('id', 'rcmcolumnindicator')
.css({ position:'absolute', 'border-right':'2px dotted #555',
'z-index':2002, height: (this.frame.offsetHeight-2)+'px' }));
this.cols = [];
this.list_pos = this.list_min_pos = lpos.left;
// save columns positions
for (i=0; i<cells.length; i++) {
this.cols[i] = cells[i].offsetWidth;
if (this.column_fixed !== null && i <= this.column_fixed) {
this.list_min_pos += this.cols[i];
}
}
}
this.col_draglayer.show();
this.col_drag_active = true;
this.triggerEvent('column_dragstart');
}
// set column indicator position
if (this.col_drag_active && this.col_draglayer) {
var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
for (i=0; i<this.cols.length; i++) {
if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
cpos += this.cols[i];
else
break;
}
// handle fixed columns on left
if (i == 0 && this.list_min_pos > pos.x)
cpos = this.list_min_pos - this.list_pos;
// empty list needs some assignment
else if (!this.list.rowcount && i == this.cols.length)
cpos -= 2;
$('#rcmcolumnindicator').css({ width: cpos+'px'});
this.triggerEvent('column_dragmove', e?e:window.event);
}
this.drag_start = false;
return false;
},
/**
* Handler for mouse up events for dragging list columns
*/
column_drag_mouse_up: function(e)
{
document.onmousemove = null;
if (this.col_draglayer) {
(this.col_draglayer).remove();
this.col_draglayer = null;
}
rcube_event.remove_listener({event:'mousemove', object:this, method:'column_drag_mouse_move'});
rcube_event.remove_listener({event:'mouseup', object:this, method:'column_drag_mouse_up'});
// remove temp divs
this.del_dragfix();
if (this.col_drag_active) {
this.col_drag_active = false;
this.focus();
this.triggerEvent('column_dragend', e);
if (this.selected_column !== null && this.cols && this.cols.length) {
var i, cpos = 0, pos = rcube_event.get_mouse_pos(e);
// find destination position
for (i=0; i<this.cols.length; i++) {
if (pos.x >= this.cols[i]/2 + this.list_pos + cpos)
cpos += this.cols[i];
else
break;
}
if (i != this.selected_column && i != this.selected_column+1) {
this.column_replace(this.selected_column, i);
}
}
}
return rcube_event.cancel(e);
},
/**
* Returns IDs of all rows in a thread (except root) for specified root
*/
row_children: function(uid)
{
if (!this.rows[uid] || !this.rows[uid].has_children)
return [];
var res = [], depth = this.rows[uid].depth,
row = this.rows[uid].obj.nextSibling;
while (row) {
if (row.nodeType == 1) {
if (r = this.rows[row.uid]) {
if (!r.depth || r.depth <= depth)
break;
res.push(r.uid);
}
}
row = row.nextSibling;
}
return res;
},
/**
* Creates a layer for drag&drop over iframes
*/
add_dragfix: function()
{
$('iframe').each(function() {
$('<div class="iframe-dragdrop-fix"></div>')
.css({background: '#fff',
width: this.offsetWidth+'px', height: this.offsetHeight+'px',
position: 'absolute', opacity: '0.001', zIndex: 1000
})
.css($(this).offset())
.appendTo(document.body);
});
},
/**
* Removes the layer for drag&drop over iframes
*/
del_dragfix: function()
{
$('div.iframe-dragdrop-fix').remove();
},
/**
* Replaces two columns
*/
column_replace: function(from, to)
{
// only supported for <table> lists
if (!this.thead || !this.thead.rows)
return;
var len, cells = this.thead.rows[0].cells,
elem = cells[from],
before = cells[to],
td = document.createElement('td');
// replace header cells
if (before)
cells[0].parentNode.insertBefore(td, before);
else
cells[0].parentNode.appendChild(td);
cells[0].parentNode.replaceChild(elem, td);
// replace list cells
for (r=0, len=this.tbody.rows.length; r<len; r++) {
row = this.tbody.rows[r];
elem = row.cells[from];
before = row.cells[to];
td = document.createElement('td');
if (before)
row.insertBefore(td, before);
else
row.appendChild(td);
row.replaceChild(elem, td);
}
// update subject column position
if (this.subject_col == from)
this.subject_col = to > from ? to - 1 : to;
else if (this.subject_col < from && to <= this.subject_col)
this.subject_col++;
else if (this.subject_col > from && to >= this.subject_col)
this.subject_col--;
if (this.fixed_header)
this.init_header();
this.triggerEvent('column_replace');
},
subject_column: function()
{
return this.subject_col + (this.checkbox_selection ? 1 : 0);
}
};
rcube_list_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
rcube_list_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
rcube_list_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;
// static
rcube_list_widget._instances = [];
diff --git a/skins/elastic/ui.js b/skins/elastic/ui.js
index 29ab11465..29795c112 100644
--- a/skins/elastic/ui.js
+++ b/skins/elastic/ui.js
@@ -1,3177 +1,3177 @@
/**
* Roundcube webmail functions for the Elastic skin
*
* Copyright (c) 2017-2018, The Roundcube Dev Team
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*
* @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
*/
"use strict";
function rcube_elastic_ui()
{
var ref = this,
mode = 'normal', // one of: large, normal, small, phone
touch = false,
ios = false,
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
},
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.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.headers_show = headers_show;
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;
this.searchbar_init = searchbar_init;
this.pretty_checkbox = pretty_checkbox;
// 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 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() { app_menu(true); return false; });
buttons.back_sidebar.on('click', function() { show_sidebar(); return false; });
buttons.back_list.on('click', function() { show_list(); return false; });
buttons.back_content.on('click', function() { show_content(true); return false; });
// Initialize search forms
$('.searchbar').each(function() { searchbar_init(this); });
// Set content frame title in parent window (exclude ext-windows and dialog frames)
if (is_framed && !rcmail.env.extwin && !parent.$('.ui-dialog:visible').length) {
if (title = $('h1.voice:first').text()) {
parent.$('#layout > .content > .header > .header-title:not(.constant)').text(title);
}
}
else if (!is_framed) {
title = $('.boxtitle:first', layout.content).detach().text();
if (!title) {
title = $('h1.voice:first').text();
}
if (title) {
$('.header > .header-title', layout.content).text(title);
}
}
// Add content frame toolbar in the footer, for content buttons and navigation
if (!is_framed && layout.content.length && !$(layout.content).is('.no-navbar')
&& !$(layout.content).children('.frame-content').length
) {
env.frame_nav = $('<div class="footer 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').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;
}
content_buttons.push(create_cloned_button(target));
});
(is_framed ? parent.UI : ref).register_content_buttons(content_buttons);
// Mail compose features
if (form = rcmail.gui_objects.messageform) {
form = $('form[name="' + form + '"]');
// Show input elements with non-empty value
// These event handlers need to be registered before rcmail 'init' event
$('#_cc, #_bcc, #_replyto, #_followupto', $('.compose-headers')).each(function() {
$(this).on('change', function() {
$('#compose' + $(this).attr('id'))[this.value ? 'removeClass' : 'addClass']('hidden');
});
});
// We put compose options outside of the main form
// Because IE/Edge (<16) does not support 'form' attribute we'll copy
// inputs into the main form as hidden fields
// TODO: Consider doing this for IE/Edge only, just set the 'form' attribute on others
$('#compose-options').find('textarea,input,select').each(function() {
var hidden = $('<input>')
.attr({type: 'hidden', name: $(this).attr('name')})
.appendTo(form);
$(this).attr('tabindex', 2)
.on('change', function() {
hidden.val(this.type != 'checkbox' || this.checked ? $(this).val() : '');
})
.change();
});
}
// Use smart recipient inputs
// This have to be after mail compose feature above
$('[data-recipient-input]').each(function() { recipient_input(this); });
// Image upload widget
$('.image-upload').each(function() { image_upload_input(this); });
// Add HTML/Plain tabs (switch) on top of textarea with TinyMCE editor
$('textarea[data-html-editor]').each(function() { html_editor_init(this); });
$('#dragmessage-menu,#dragcontact-menu').each(function() {
rcmail.gui_object('dragmenu', this.id);
});
// Taskmenu items added by plugins do not use elastic classes (e.g help plugin)
// it's for larry skin compat. We'll assign '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 (button && (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
$('[data-hidden]').each(function() {
var m, v = $(this).data('hidden'),
parent = $(this).parent('li'),
re = /(large|big|small|phone)/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');
};
(new MutationObserver(callback)).observe(list, {childList: true, subtree: true});
callback();
});
};
/**
* 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)
{
var button = $('<a>'),
target_id = target.attr('id'),
button_id = target_id + '-clone',
btn_class = target[0].className;
btn_class = $.trim(btn_class.replace('btn-primary', 'primary').replace(/(btn[a-z-]*|button|disabled)/g, ''))
btn_class += ' button disabled';
button.attr({'onclick': '', id: button_id, href: '#', 'class': btn_class})
.append($('<span class="inner">').text(target.text()))
.on('click', function(e) { target.click(); });
if (is_framed) {
button.data('target', target);
frame_buttons.push($.extend({button_id: button_id}, find_button(target[0].id)));
}
else {
// Register the button to get active state updates
register_cloned_button(target_id, button_id, btn_class.replace(' disabled', ''));
}
return button;
};
/**
* Finds an rcmail button
*/
function find_button(id)
{
var i, button, command;
for (command in rcmail.buttons) {
for (i = 0; i < rcmail.buttons[command].length; i++) {
button = rcmail.buttons[command][i];
if (button.id == id) {
return {
command: command,
index: i,
data: button
};
}
}
}
};
/**
* Setup environment
*/
function layout_init()
{
// Select current layout element
env.last_selected = $('#layout > div.selected')[0];
if (!env.last_selected && layout.content.length) {
$.each(['sidebar', 'list', 'content'], function() {
if (layout[this].length) {
env.last_selected = layout[this][0];
layout[this].addClass('selected');
return false;
}
});
}
// Register resize handler
$(window).on('resize', function() {
clearTimeout(env.resize_timeout);
env.resize_timeout = setTimeout(function() { resize(); }, 25);
});
// Enable rcmail.open_window intercepting
env.open_window = rcmail.open_window;
rcmail.open_window = window_open;
rcmail
.addEventListener('message', message_displayed)
.addEventListener('menu-open', menu_toggle)
.addEventListener('menu-close', menu_toggle)
.addEventListener('editor-init', tinymce_init)
.addEventListener('autocomplete_create', rcmail_popup_init)
.addEventListener('googiespell_create', rcmail_popup_init)
.addEventListener('setquota', update_quota)
.addEventListener('enable-command', enable_command_handler)
.addEventListener('init', init);
};
/**
* rcmail 'init' event handler
*/
function init()
{
// Additional functionality on list widgets
$('table[data-list]').each(function() {
var button,
table = $(this),
list = table.data('list');
if (rcmail[list] && rcmail[list].multiselect) {
var parent = table.parents('.sidebar,.list,.content'),
toolbar = parent.find('.pagenav');
- if (!toolbar) {
+ if (!toolbar.length) {
toolbar = $('<div class="pagenav toolbar footer small">').appendTo(parent);
}
// Enable checkbox selection on list widgets
- rcmail[list].checkbox_selection = true;
+ rcmail[list].enable_checkbox_selection();
// Add Select button to the list navigation bar
button = $('<a>').attr({'class': 'button icon toggleselect disabled', role: 'button'})
.on('click', function() { if ($(this).is('.active')) table.toggleClass('withselection'); })
.append($('<span class="inner">').text(rcmail.gettext('select')))
.prependTo(toolbar);
// Update Select button state on list update
rcmail.addEventListener('listupdate', function(prop) {
if (prop.list && prop.list == rcmail[list]) {
if (prop.rowcount) {
button.addClass('active').removeClass('disabled').attr('tabindex', 0);
}
else {
button.removeClass('active').addClass('disabled').attr('tabindex', -1);
}
}
});
}
// https://github.com/roundcube/elastic/issues/45
// Draggable blocks scrolling on touch devices, we'll disable it there
if (touch && rcmail[list]) {
if (typeof rcmail[list].draggable == 'function') {
rcmail[list].draggable('destroy');
}
else if (typeof rcmail[list].draggable == 'boolean') {
rcmail[list].draggable = false;
}
}
});
// Display "List is empty..." on the list
if (window.MutationObserver) {
$('[data-label-msg]').filter('ul,table').each(function() {
var fn, observer, callback,
info = $('<div class="listing-info hidden">').insertAfter(this),
table = $(this),
fn = function() {
var ext, command,
msg = table.data('label-msg'),
list = table.is('ul') ? table : table.children('tbody');
if (!rcmail.env.search_request && !rcmail.env.qsearch
&& msg && !list.children(':visible').length
) {
ext = table.data('label-ext');
command = table.data('create-command');
if (ext && (!command || rcmail.commands[command])) {
msg += ' ' + ext;
}
info.text(msg).removeClass('hidden');
return;
}
info.addClass('hidden');
};
callback = function() {
// wait until the UI stops loading and the list is visible
if (rcmail.busy || !table.is(':visible')) {
return setTimeout(callback, 250);
}
clearTimeout(env.list_timer);
env.list_timer = setTimeout(fn, 50);
};
// show/hide the message when something changes on the list
observer = new MutationObserver(callback);
observer.observe(table[0], {childList: true, subtree: true, attributes: true, attributeFilter: ['style']});
// initialize the message
callback();
});
}
// Create floating action button(s)
if ((layout.list.length || layout.content.length) && is_mobile()) {
var fabuttons = [];
$('[data-fab]').each(function() {
var button = $(this),
task = button.data('fab-task') || '*',
action = button.data('fab-action') || '*';
if ((task == '*' || task == rcmail.task)
&& (action == '*' || action == rcmail.env.action || (action == 'none' && !rcmail.env.action))
) {
fabuttons.push(create_cloned_button(button));
}
});
if (fabuttons.length) {
$('<div class="floating-action-buttons">').append(fabuttons)
.appendTo(layout.list.length ? layout.list : layout.content);
}
}
// Add menu link for each attachment
if (rcmail.env.action != 'print') {
$('#attachment-list > li').each(function() {
attachmentmenu_append(this);
});
}
var phone_confirmation = function(label) {
if (mode == 'phone') {
rcmail.display_message(rcmail.gettext(label), 'confirmation');
}
};
rcmail.addEventListener('fileappended', function(e) {
if (e.attachment.complete) {
attachmentmenu_append(e.item);
if (e.attachment.mimetype == 'text/vcard' && rcmail.commands['attach-vcard']) {
phone_confirmation('vcard_attachments.vcardattached');
}
}
})
.addEventListener('managesieve.insertrow', function(o) { bootstrap_style(o.obj); })
.addEventListener('add-recipient', function() { phone_confirmation('recipientsadded'); });
rcmail.init_pagejumper('.pagenav > input');
if (rcmail.task == 'mail') {
if (rcmail.env.action == 'compose') {
// In compose window we do not provide "Back' button, instead
// we modify the Mail button in the task menu to act like it (i.e. calls 'list' command)
if (!rcmail.env.extwin) {
$('a.button.mail', layout.menu).attr('onclick', "return rcmail.command('list','',this,event)");
}
rcmail.addEventListener('compose-encrypted', function(e) {
$("a.mode-html, button.attach").prop('disabled', e.active);
$('a.button.attach, a.button.responses')[e.active ? 'addClass' : 'removeClass']('disabled');
});
$('.sidebar > .footer > a.button').click(function() {
if ($(this).is('.disabled')) {
rcmail.display_message(rcmail.gettext('nocontactselected'), 'warning');
}
});
}
// 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.env.thread_padding = '1.5rem';
// Update layout after initialization (again)
// In devel mode we have to wait until all styles are applied by less
if (rcmail.env.devel_mode && window.less) {
less.pageLoadFinished.then(function() {
resize();
});
}
else {
resize();
}
// Add date format placeholder to datepicker inputs
var func, format;
if (format = rcmail.env.date_format_localized) {
func = function(input) { $(input).filter('.datepicker').attr('placeholder', format); };
$('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;
}
$('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,[type=button],[type=file],[type=radio],[type=checkbox]),select,textarea';
$(supported_controls, $('.propform', context)).addClass('form-control');
$('[type=checkbox]', $('.propform', context)).addClass('form-check-input');
$('table.propform', context).each(function() {
var text_rows = 0, form_rows = 0;
$(this).find('> 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 == 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(':not(:first)').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');
});
});
// 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:first', fieldset).text())
.click(function() {
$(this).tab('show');
// Returning false here prevents from strange scrolling issue
// when the form is in an iframe, e.g. contact edit form
return false;
})
);
$('legend:first', fieldset).hide();
tabs.push(tab);
});
// create the navigation bar
nav.append(tabs).insertBefore(item);
// activate the first tab
$('a.nav-link:first', nav).click();
});
// Make tables pretier
$('table:not(.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');
});
$('.toolbarmenu select', context).addClass('form-control');
if (context != document) {
$(supported_controls, context).addClass('form-control');
}
// 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], .toolbarmenu 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); });
}
// Make message-objects alerts pretty (the same as UI alerts)
$('#message-objects', context).children(':not(.ui.alert)').each(function() {
// message objects with notice class are really warnings
var cl = $(this).removeClass('notice').attr('class').split(/\s/)[0] || 'warning';
alert_style(this, cl);
$(this).addClass('box' + cl);
$('a', this).addClass('btn btn-primary');
});
// Style calendar widget (we use setTimeout() because there's no widget event we could bind to)
$('input.datepicker', context).focus(function() {
setTimeout(function() { bootstrap_style($('.ui-datepicker')); }, 5);
});
// Form validation errors (managesieve plugin)
$('.error', context).addClass('is-invalid');
// Make logon form prettier
if (rcmail.env.task == 'login' && context == document) {
$('#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('form-control')
.attr('placeholder', label.text())
.before($('<span class="input-group-prepend">').append(icon))
.parent().addClass('input-group');
});
}
};
/**
* Initializes popup menus
*/
function dropdowns_init()
{
$('[data-popup]').each(function() { popup_init(this); });
$(document).on('click', popups_close);
rcube_webmail.set_iframe_events({mousedown: popups_close, touchstart: popups_close});
};
/**
* Init content frame
*/
function content_frame_init()
{
var last_selected = env.last_selected,
title_reset = function(title) {
if (typeof title !== 'string' || !title.length) {
title = $('h1.voice').text() || $('title').text() || '';
}
$('.header > .header-title', layout.content).text(title);
};
// display or reset the content frame
var common_content_handler = function(e, href, show, title)
{
content_frame_navigation(href, e);
if (show && !layout.content.is(':visible')) {
env.last_selected = layout.content[0];
screen_resize();
if (title) {
title_reset(title);
}
}
else if (!show) {
if (env.last_selected != last_selected && !env.content_lock) {
env.last_selected = last_selected;
screen_resize();
}
title_reset();
}
env.content_lock = false;
};
// display the list widget after 'list' and 'listgroup' commands
var common_list_handler = function(e) {
if (mode != 'large' && !env.content_lock && e.list) {
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' && !rcmail.env.action)) {
args.list = 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.list = true;
common_list_handler(e);
})
.addEventListener('show-content', function(e) {
if (!$(e.obj).is('iframe')) {
$(e.scrollElement || e.obj).scrollTop(0);
if (is_mobile()) {
iframe_loader(e.obj);
}
}
common_content_handler(e.event || new Event, '_action=' + (e.mode || 'edit'), true, e.title);
});
};
/**
* Content frame navigation
*/
function content_frame_navigation(href, event)
{
// Don't display navigation for create/add action frames
if (href.match(/_action=(create|add)/) || href.match(/_nav=hide/)) {
$(env.frame_nav).addClass('hide-nav-buttons');
return;
}
var node, uid, list, _list = $('[data-list]', layout.list).data('list');
if (!_list || !(list = rcmail[_list])) {
// hide navbar if there are no visible buttons, e.g. Help plugin UI
if ($(env.frame_nav).is('.hide-nav-buttons') && !$('.buttons', env.frame_nav).children().length) {
$(env.frame_nav).addClass('hidden');
}
return;
}
$(env.frame_nav).removeClass('hide-nav-buttons hidden');
// expand collapsed row so we do not skip the whole thread
// TODO: Unified interface for list and treelist widgets
if (uid = list.get_single_selection()) {
if (list.rows && list.rows[uid] && !list.rows[uid].expanded) {
list.expand_row(event, uid);
}
else if (list.get_node && (node = list.get_node(uid)) && node.collapsed) {
list.expand(uid);
}
}
var prev, next,
frame = $('#' + rcmail.env.contentframe),
next_button = $('a.button.next', env.frame_nav).off('click').addClass('disabled'),
prev_button = $('a.button.prev', env.frame_nav).off('click').addClass('disabled');
if ((next = list.get_next()) || rcmail.env.current_page < rcmail.env.pagecount) {
next_button.removeClass('disabled').on('click', function() {
env.content_lock = true;
iframe_loader(frame);
if (next) {
list.select(next);
}
else {
rcmail.env.list_uid = 'FIRST';
rcmail.command('nextpage');
}
});
}
if (((prev = list.get_prev()) && (prev != '*' || _list != 'subscription_list')) || rcmail.env.current_page > 1) {
prev_button.removeClass('disabled').on('click', function() {
env.content_lock = true;
iframe_loader(frame);
if (prev) {
list.select(prev);
}
else {
rcmail.env.list_uid = 'LAST';
rcmail.command('previouspage');
}
});
}
};
/**
* Handler for editor-init event
*/
function tinymce_init(o)
{
// Enable autoresize plugin
o.config.plugins += ' autoresize';
if (is_touch()) {
// Make the toolbar icons bigger
o.config.toolbar_items_size = null;
// Use minimalistic toolbar
o.config.toolbar = 'undo redo | insert | styleselect';
if (o.config.plugins.match(/emoticons/)) {
o.config.toolbar += ' emoticons';
}
}
};
/**
* Handler for some Roundcube core popups
*/
function rcmail_popup_init(o)
{
// Add some common styling to the autocomplete/googiespell popups
$('table,ul', o.obj).addClass('toolbarmenu 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(); });
}
$('table,button', o.obj).click(function(e) {
if (!$(e.target).is('input')) {
$('.popover-overlay').remove();
}
});
}
};
/**
* Handler for 'enable-command' event
*/
function enable_command_handler(args)
{
if (is_framed) {
$.each(frame_buttons, function(i, button) {
if (args.command == button.command) {
parent.$('#' + button.button_id)[args.status ? 'removeClass' : 'addClass']('disabled');
}
});
}
if (rcmail.task == 'mail') {
switch (args.command) {
case 'reply-list':
if (rcmail.env.reply_all_mode == 1) {
var label = rcmail.gettext(args.status ? 'replylist' : 'replyall');
$('a.button.reply-all').attr('title', label).find('.inner').text(label);
}
break;
case 'compose-encrypted':
// show the toolbar button for Mailvelope
if (args.status) {
$('a.button.encrypt').parent().show();
}
break;
case 'compose-encrypted-signed':
// enable selector for encrypt and sign
$('#encryption-menu-button').show();
break;
case 'mark':
// show the toolbar button for Mailvelope
$('a.button.markmessage')[args.status ? 'removeClass' : 'addClass']('disabled');
break;
}
}
};
/**
* Window resize handler
* Does layout reflows e.g. on screen orientation change
*/
function resize()
{
var size, mobile, 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;
screen_resize();
screen_resize_html();
// disable ext-windows and other features
if (mobile = is_mobile()) {
rcmail.set_env(env.small_screen_config);
rcmail.enable_command('extwin', false);
}
else {
rcmail.set_env(env.config);
rcmail.enable_command('extwin', true);
}
// Hide content frame buttons on small devices (with frame toolbar in parent window)
$.each(content_buttons, function() { $(this)[mobile ? 'hide' : 'show'](); });
};
function screen_resize()
{
if (is_framed && !layout.sidebar.length && !layout.list.length) {
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_headers();
// On iOS and Android the content frame height is never correct, fix it
if (bw.webkit) {
$('.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');
}
};
/**
* Sets left and right margin to the header title element to make it
* properly centered depending on the number of buttons on both sides
*/
function screen_resize_headers()
{
$('#layout > div > .header').each(function() {
var title, right = 0, left = 0, padding = 0,
sizes = {left: 0, right: 0};
$(this).children(':visible').each(function() {
if (!title && $(this).is('.header-title')) {
title = $(this);
return;
}
sizes[title ? 'right' : 'left'] += this.offsetWidth;
});
if (padding + sizes.right >= sizes.left) {
right = 0;
left = sizes.right + padding - sizes.left;
}
else {
left = 0;
right = sizes.left - (padding + sizes.right);
}
$(title).css({
'margin-right': right + 'px',
'margin-left': left + 'px',
'padding-right': padding + 'px'
});
});
};
function screen_resize_phone()
{
screen_resize_small_all();
app_menu(false);
};
function screen_resize_small()
{
screen_resize_small_all();
app_menu(true);
};
function screen_resize_normal()
{
var show;
if (layout.list.length) {
show = layout.list.is(env.last_selected) || (!layout.sidebar.is(env.last_selected) && !layout.sidebar.is('.layout-sticky'));
layout.list[show ? 'removeClass' : 'addClass']('hidden');
}
if (layout.sidebar.length) {
show = !layout.list.length || layout.sidebar.is(env.last_selected) || layout.sidebar.is('.layout-sticky');
layout.sidebar[show ? 'removeClass' : 'addClass']('hidden');
}
layout.content.removeClass('hidden');
app_menu(true);
screen_resize_small_none();
$('.header > ul.toolbar', layout.list).addClass('popupmenu');
};
function screen_resize_large()
{
$.each(layout, function(name, item) { item.removeClass('hidden'); });
screen_resize_small_none();
};
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');
}
if (layout.list.length) {
show = !got_content && layout.list.is(env.last_selected);
layout.list[show ? 'removeClass' : 'addClass']('hidden');
}
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();
}
$('.header > ul.toolbar', layout.content).addClass('popupmenu');
$('.header > ul.toolbar', layout.list).addClass('popupmenu');
};
function screen_resize_small_none()
{
buttons.back_list.filter(function() { return $(this).parents('.sidebar').length == 0; }).hide();
$('ul.toolbar.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">').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');
/*
$('a', p.object).addClass('alert-link');
// show a popup dialog on errors
if (p.type == 'error' && rcmail.env.task != 'login') {
// hide original message object, we don't want both
rcmail.hide_message(p.object);
}
*/
};
/**
* 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',
vcardattachment: 'alert-info' /* vcard_attachments plugin */
};
if (wrap && addicon && !$(object).is('.aligned-buttons')) {
// we need the content to be non-text node for best alignment
tmp = $(object).html();
$(object).html($('<span>').html(tmp));
}
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%');
}
}
// Display loader when the dialog has an iframe
iframe_loader($('div.popup > iframe', me));
// TODO: style buttons/forms
bootstrap_style(dialog.uiDialog);
};
/**
* Initializes searchbar widget
*/
function searchbar_init(bar)
{
var options_button = $('a.button.options', bar),
input = $('input:not([type=hidden])', bar),
form = $('form', bar),
is_search_pending = function() {
if (input.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;
}
},
update_func = function() {
$(bar)[is_search_pending() ? 'addClass' : 'removeClass']('active');
};
options_button.on('click', function(e) {
var id = $(this).data('target'),
options = $('#' + id),
open = options.is(':visible');
if (options.length) {
if (!open) {
if (ref[id]) {
ref[id](options.get(0), this, e);
}
else if (typeof window[id] == 'function') {
window[id](options.get(0), this, e);
}
}
options.next()[open ? 'show' : 'hide']();
options.toggleClass('hidden');
$('.floating-action-buttons').toggleClass('hidden');
$(bar).toggleClass('open');
$('button.search', options).off('click.search').on('click.search', function() {
options_button.trigger('click');
});
}
});
input.on('input change', update_func);
// 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});
// 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('---');
}
update_func();
});
rcmail.addEventListener('init', function() {
update_func();
if (rcmail.gui_objects.search_filter) {
$(rcmail.gui_objects.search_filter).on('change', update_func);
}
if (rcmail.gui_objects.foldersfilter) {
$(rcmail.gui_objects.foldersfilter).on('change', update_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 items = [],
list_items = [],
button_func = function(button, items) {
var item = $('<li role="menuitem">'),
button = $(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 toolbar to a popup list
if (layout.list) {
$('.header > .toolbar', layout.list).each(function() {
var toolbar = $(this);
toolbar.children().each(function() { button_func(this, list_items); });
toolbar.remove();
});
}
// convert toolbar to a popup list
if (layout.content) {
$('.header > .toolbar', layout.content).each(function() {
var toolbar = $(this);
toolbar.children().each(function() { button_func(this, 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': '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'});
container
// TODO: copy original toolbar attributes (class, role, aria-*)
.append($('<ul>').attr(menu_attrs).data('popup-parent', container).append(list_items))
.append(menu_button);
}
// append the new toolbar and menu button
if (items.length) {
var container = layout.content.children('.header'),
menu_attrs = {'class': 'toolbar popupmenu listing iconized', id: 'toolbar-menu'},
menu_button = $('<a class="button icon toolbar-menu-button" href="#menu">')
.attr({'data-popup': 'toolbar-menu'});
container
// TODO: copy original toolbar attributes (class, role, aria-*)
.append($('<ul>').attr(menu_attrs).data('popup-parent', container).append(items))
.append(menu_button);
if (layout.list.length) {
// bind toolbar menu with the menu button in the list header
$('a.toolbar-menu-button', layout.list).click(function(e) {
e.stopPropagation();
menu_button.click();
});
}
}
};
/**
* Initialize a popup for specified button element
*/
function popup_init(item, win)
{
// On mobile we display the menu from the frame in the parent window
if (is_framed && is_mobile()) {
return parent.UI.popup_init(item, win || window);
}
if (!win) win = window;
var level,
popup_id = $(item).data('popup'),
popup = $(win.$('#' + popup_id).get(0)), // a "hack" to support elements in frames
popup_orig = popup,
title = $(item).attr('title'),
content_element = function() {
// On mobile we display a menu from the frame in the parent window
// To make menu actions working we have to clone the menu
// and pass click events to it...
if (win != window) {
popup = popup_orig.clone(true, true);
popup.attr('id', popup_id + '-clone')
.appendTo(document.body)
.find('li > a, li.checkbox > label').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);
}
});
if (popup_id && menus[popup_id]) {
menus[popup_id].transitioning = false;
}
// add overlay element for phone layout
if (mobile && !$('.popover-overlay').length) {
$('<div>').attr('class', 'popover-overlay')
.appendTo('body')
.click(function() { $(this).remove(); });
}
$('.popover-body', popover).addClass('webkit-scroller');
})
.on('hide.bs.popover', function() {
if (level == 1) {
$('.popover-overlay').remove();
}
if (popup_id && menus[popup_id] && popup.is(':visible')) {
menus[popup_id].transitioning = true;
}
})
.on('hidden.bs.popover', function() {
if (/-clone$/.test(popup.attr('id'))) {
popup.remove();
}
else {
popup.attr('aria-hidden', true)
// Some menus aren't being hidden, force that
.addClass('hidden')
// Bootstrap will detach the popup element from
// the DOM (https://github.com/twbs/bootstrap/issues/20219)
// making our menus to not update buttons state.
// Work around this by attaching it back to the DOM tree.
popup.appendTo(popup.data('popup-parent') || document.body);
}
// close orphaned popovers, for some reason there are sometimes such dummy elements left
$('.popover-body:empty').each(function() { $(this).parent().remove(); });
if (popup_id && menus[popup_id]) {
delete menus[popup_id];
}
})
.on('keypress', function(event) {
// Close the popup on ESC key
if (event.originalEvent.keyCode == 27) {
$(item).popover('hide');
}
});
// 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)
{
$('.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:first', p.obj),
target = p.props && p.props.link ? p.props.link : p.originalEvent.target;
if ($(target).is('span')) {
target = $(target).parents('a,li')[0];
}
if (p.name.match(/^drag/)) {
// create a fake element to position drag menu on the cursor position
pos = rcube_event.get_mouse_pos(p.originalEvent);
target = $('<a>').css({
position: 'absolute',
left: pos.x,
top: pos.y,
height: '1px',
width: '1px',
visibility: 'hidden'
})
.appendTo(document.body).get(0);
}
pos = $(target).data('popup-pos') || 'right';
if (p.name == 'folder-selector') {
content.addClass('listing folderlist');
}
else if (p.name == 'addressbook-selector' || p.name == 'contactgroup-selector') {
content.addClass('listing contactlist');
}
else if (content.hasClass('toolbarmenu')) {
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({
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();
// 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,
width: width
});
};
/**
* 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',
width: 600,
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',
width: 600,
height: 400
});
};
/**
* Search options menu popup
*/
function searchmenu(obj)
{
var n, all,
list = $('input[name="s_mods[]"]', obj),
scope_select = $('select[name=s_scope]', obj),
mbox = rcmail.env.mailbox,
mods = rcmail.env.search_mods,
scope = rcmail.env.search_scope || 'base';
if (!$(obj).data('initialized')) {
list.on('click', function() { set_searchmod(this, obj); });
scope_select.on('click', function() { rcmail.set_searchscope($(this).val()); });
$(obj).data('initialized', true);
}
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(elem, menu)
{
var all, m, task = rcmail.env.task,
mods = rcmail.env.search_mods,
mbox = rcmail.env.mailbox,
scope = $('select[name=s_scope]', 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';
}
else { //addressbook
m = mods;
all = '*';
}
if (!elem.checked) {
delete(m[elem.value]);
}
else {
m[elem.value] = 1;
}
// mark all fields
if (elem.value == all) {
$('input[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="toolbarmenu 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');
}
});
};
/**
* 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) {
rcmail.command(action + '-attachment', id, this, e.originalEvent);
});
});
// call menu-open so core can set state of menu commands
rcmail.command('menu-open', {menu: 'attachmentmenu', id: id}, obj, event);
};
/**
* Appends drop-icon to attachments list item (to invoke attachment menu)
*/
function attachmentmenu_append(item)
{
item = $(item);
if (!item.is('.no-menu') && !item.children('.drop').length) {
var label = rcmail.gettext('options');
var button = $('<a>')
.attr({
href: '#',
tabindex: 0,
title: label,
'class': 'button icon dropdown skip-content'
})
.on('click', function(e) {
attachmentmenu($('#attachmentmenu'), button, e);
})
.append($('<span>').attr('class', 'inner').text(label))
.appendTo(item);
}
};
/**
* Mailto menu
*/
function mailtomenu(obj, button, event)
{
var mailto = $(button).attr('href').replace(/^mailto:/, '');
if (mailto.indexOf('@') < 0) {
return true; // let the browser handle this
}
if (rcmail.env.has_writeable_addressbook) {
$('.addressbook', obj).addClass('active')
.off('click').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 + '>';
}
rcmail.command('add-contact', contact, this, e.originalEvent);
});
}
$('.compose', obj).off('click').on('click', function(e) {
rcmail.command('compose', mailto, this, e.originalEvent);
});
return rcmail.command('menu-open', {menu: 'mailto-menu', link: button}, button, event);
};
/**
* Appends popup menu to mailto links
*/
function mailtomenu_append(item)
{
$(item).attr('onclick', '').on('click', function(e) {
return mailtomenu($('#mailto-menu'), item, e);
});
};
/**
* Headers menu in mail compose
*/
function headersmenu(obj, button, event)
{
$('li > a', obj).each(function() {
var target = '#compose_' + $(this).data('target');
$(this)[$(target).is(':visible') ? 'removeClass' : 'addClass']('active')
.off().on('click', function() {
$(target).removeClass('hidden').find('.recipient-input > input').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');
element.attr('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'});
});
}
};
/**
* Replaces recipient input with content-editable element that uses "recipient boxes"
*/
function recipient_input(obj)
{
var list, input, ac_props,
input_len_update = function() {
input.css('width', input.val().length * 10 + 15);
},
apply_func = function() {
// update the original input
$(obj).val(list.text() + input.val());
},
focus_func = function() {
list.addClass('focus');
// move cursor to the end of input text, use setTimeout for Firefox
setTimeout(function() { rcmail.set_caret_pos(input.get(0), input.val().length); }, 1);
},
insert_recipient = function(name, email, replace) {
var recipient = $('<li class="recipient">'),
name_element = $('<span class="name">').html(recipient_input_name(name || email))
.on('dblclick', function(e) { recipient_input_edit_dialog(e, insert_recipient); }),
email_element = $('<span class="email">'),
// TODO: should the 'close' link have tabindex?
link = $('<a>').attr({'class': 'button icon remove'})
.click(function() {
recipient.remove();
apply_func();
input.focus();
return false;
});
if (name) {
email = ' <' + email + '>';
}
email_element.text((name ? email : '') + ',');
recipient.attr('title', name ? (name + email) : null)
.append([name_element, email_element, link])
if (replace)
replace.replaceWith(recipient);
else
recipient.insertBefore(input.parent());
},
update_func = function() {
var 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();
if (result.recipients.length) {
return true;
}
},
parse_func = function(e) {
// Note it can be also executed when autocomplete inserts a recipient
update_func();
if (e.type == 'blur') {
list.removeClass('focus');
}
},
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 character (,;) was pressed
else if (e.key == ',' || e.key == ';') {
if (update_func()) {
return false;
}
}
input_len_update();
};
// Create the input elemennt and "editable" area
input = $('<input>').attr({type: 'text', tabindex: $(obj).attr('tabindex')})
.on('paste change blur', parse_func)
.on('keydown', keydown_func)
.on('focus mousedown', focus_func);
list = $('<ul>').addClass('form-control recipient-input')
.append($('<li>').append(input))
.on('click', function() { input.focus(); });
// "Replace" the original input/textarea with the content-editable div
// Note: we do not remove the original element, and we do not use
// display: none, because we want to handle onfocus event
// Note: tabindex:-1 to make Shift+TAB working on these widgets
$(obj).css({position: 'absolute', opacity: 0, left: '-5000px', width: '10px'})
.attr('tabindex', -1)
.after(list)
// some core code sometimes focuses or changes the original node
// in such cases we wan't to parse it's value and apply changes
// to the widget element
.on('focus', function(e) { input.focus(); })
.on('change', function(e) {
$('li.recipient', list).remove();
input.val(this.value).change();
})
// copy and parse the value already set
.change();
// this one line is here to fix border of Bootstrap's input-group,
// input-group should not contain any hidden elements
$(obj).detach().insertBefore(list.parent());
if (rcmail.env.autocomplete_threads > 0) {
ac_props = {
threads: rcmail.env.autocomplete_threads,
sources: rcmail.env.autocomplete_sources
};
}
// Init autocompletion
rcmail.init_address_input_events(input, ac_props);
};
/**
* Parses recipient address input and extracts recipients from it
*/
function recipient_input_parser(text)
{
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(/^[,;]/, '');
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 += '&lt;';
break;
case '>':
result += '&gt;';
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; });
$(obj).append(reset_button).click(function() { rcmail.upload_input('upload-form'); });
$('img', obj).on('load', function() {
// FIXME: will that work in IE?
var state = (this.currentSrc || this.src).indexOf(rcmail.env.photo_placeholder) != -1;
$(obj)[state ? 'removeClass' : 'addClass']('changed');
});
};
/**
* Displays loading... overlay for iframes
*/
function iframe_loader(frame)
{
frame = $(frame);
if (frame.length) {
var loader = $('<div>').attr('class', 'iframe-loader')
.append($('<div>').attr('class', 'spinner').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');
}
}
};
/**
* Checkbox wrapper
*/
function pretty_checkbox(checkbox)
{
var checkbox = $(checkbox),
id = checkbox.attr('id');
if (checkbox.is('.icon-checkbox')) {
return;
}
if (!id) {
if (!env.icon_checkbox) env.icon_checkbox = 0;
id = 'icochk' + (++env.icon_checkbox);
checkbox.attr('id', id);
}
checkbox.addClass('icon-checkbox form-check-input').after(
$('<label>').attr({'for': id, title: checkbox.attr('title') || ''})
.on('click', function(e) { e.stopPropagation(); })
);
};
/**
* HTML editor textarea wrapper with nice looking tabs-like switch
*/
function html_editor_init(obj)
{
// Here we support two structures
// 1. <div><textarea></textarea><select name="editorSelector"></div>
// 2. <tr><td><td><td><textarea></textarea></td></tr>
// <tr><td><td><td><input type="checkbox"></td></tr>
var sw, is_table = false,
editor = $(obj),
parent = editor.parent(),
mode = function() {
if (is_table) {
return sw.is(':checked') ? 'html' : 'plain';
}
return sw.val();
},
tabs = $('<ul class="nav nav-tabs">')
.append($('<li class="nav-item">')
.append($('<a class="nav-link mode-html" href="#">')
.text(rcmail.gettext('htmltoggle'))))
.append($('<li class="nav-item">')
.append($('<a class="nav-link mode-plain" href="#">')
.text(rcmail.gettext('plaintoggle'))));
if (parent.is('td')) {
sw = $('input[type="checkbox"]', parent.parent().next());
is_table = true;
}
else {
sw = $('[name="editorSelector"]', obj.form);
}
// sanity check
if (sw.length != 1) {
return;
}
parent.addClass('html-editor');
editor.before(tabs);
$('a', tabs).attr('tabindex', editor.attr('tabindex'))
.on('click', function(e) {
var id = editor.attr('id'), is_html = $(this).is('.mode-html');
e.preventDefault();
if (rcmail.command('toggle-editor', {id: id, html: is_html}, '', e.originalEvent)) {
$(this).tab('show');
if (is_table) {
sw.prop('checked', is_html);
}
}
})
.filter('.mode-' + mode()).tab('show');
if (is_table) {
// Hide unwanted table cells
sw.parent().parent().hide();
parent.prev().hide();
// Modify the textarea cell to use 100% width
parent.addClass('col-sm-12');
}
// make the textarea autoresizeable
textarea_autoresize_init(editor);
};
/**
* Make the textarea autoresizeable depending on it's content length.
* The way there's no vertical scrollbar.
*/
function textarea_autoresize_init(textarea)
{
var resize = function(e) {
clearTimeout(env.textarea_timer);
env.textarea_timer = setTimeout(function() {
var area = $(e.target),
initial_height = area.data('initial-height'),
scroll_height = area[0].scrollHeight;
// do nothing when the area is hidden
if (!scroll_height) {
return;
}
if (!initial_height) {
area.data('initial-height', initial_height = scroll_height);
}
// strange effect in Chrome/Firefox when you delete a line in the textarea
// the scrollHeight is not decreased by the line height, but by 2px
// so jumps up many times in small steps, we'd rather use one big step
if (area.outerHeight() - scroll_height == 2) {
scroll_height -= 19; // 21px is the assumed line height
}
area.outerHeight(Math.max(initial_height, scroll_height));
}, 10);
};
$(textarea).css('overflow-y', 'hidden').on('input', resize).trigger('input');
// Make sure the height is up-to-date also in time intervals
setInterval(function() { $(textarea).trigger('input'); }, 1000);
};
// Inititalizes smart list input
function smart_field_init(field)
{
var tip, id = field.id + '_list',
area = $('<div class="multi-input"><div class="content"></div><div class="invalid-feedback"></div></div>'),
list = field.value ? field.value.split("\n") : [''];
if ($('#' + id).length) {
return;
}
// add input rows
$.each(list, function(i, v) {
smart_field_row_add($('.content', area), v, field.name, i, $(field).data('size'));
});
area.attr('id', id);
field = $(field);
if (field.attr('disabled')) {
area.hide();
}
// disable the original field anyway, we don't want it in POST
else {
field.prop('disabled', true);
}
if (field.data('hidden')) {
area.hide();
}
field.after(area);
if (field.hasClass('is-invalid')) {
area.addClass('is-invalid');
$('.invalid-feedback', area).text(field.data('error-msg'));
}
};
function smart_field_row_add(area, value, name, idx, size, after)
{
// build row element content
var input, elem = $('<div class="input-group">'
+ '<input type="text" class="form-control">'
+ '<span class="input-group-append"><a class="icon reset input-group-text" href="#"></a></span>'
+ '</div>'),
attrs = {value: value, name: name + '[]'};
if (size) {
attrs.size = size;
}
input = $('input', elem).attr(attrs)
.keydown(function(e) {
var input = $(this);
// element creation event (on Enter)
if (e.which == 13) {
var name = input.attr('name').replace(/\[\]$/, ''),
dt = (new Date()).getTime(),
elem = smart_field_row_add(area, '', name, dt, size, input.parent());
$('input', elem).focus();
}
// backspace or delete: remove input, focus previous one
else if ((e.which == 8 || e.which == 46) && input.val() == '') {
var parent = input.parent(),
siblings = area.children();
if (siblings.length > 1) {
if (parent.prev().length) {
parent.prev().children('input').focus();
}
else {
parent.next().children('input').focus();
}
parent.remove();
return false;
}
}
});
// element deletion event
$('a.reset', elem).click(function() {
var record = $(this.parentNode.parentNode);
if (area.children().length > 1) {
$('input', record.next().length ? record.next() : record.prev()).focus();
record.remove();
}
else {
$('input', record).val('').focus();
}
});
$(elem).find('input,a')
.on('focus', function() { area.addClass('focused'); })
.on('blur', function() { area.removeClass('focused'); });
if (after) {
after.after(elem);
}
else {
elem.appendTo(area);
}
return elem;
};
// Reset and fill the smart list input with new data
function smart_field_reset(field, data)
{
var id = field.id + '_list',
list = data.length ? data : [''],
area = $('#' + id).children('.content');
area.empty();
// add input rows
$.each(list, function(i, v) {
smart_field_row_add(area, v, field.name, i, $(field).data('size'));
});
};
/**
* Register form errors, mark fields as invalid, dsplay the error below the input
*/
function form_errors(tips)
{
$.each(tips, function() {
var input = $('#' + this[0]).addClass('is-invalid');
if (input.data('type') == 'list') {
input.data('error-msg', this[2]);
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.button', obj),
navlist = $(obj).next();
if (!navlist.height()) {
records = $('tr,li', navlist).filter(function() { return this.style.display != 'none'; });
height = $(records[0]).height() || 50;
navlist.animate({height: (Math.min(5, records.length) * height + 1) + 'px'}, speed);
button.addClass('collapse').removeClass('expand');
$(obj).addClass('expanded');
}
else {
navlist.animate({height: '0'}, speed);
button.addClass('expand').removeClass('collapse');
$(obj).removeClass('expanded');
}
};
/**
* Wrapper for rcmail.open_window to intercept window opening
* and display a dialog with an iframe instead of a real window.
*/
function window_open(url)
{
if (!is_mobile()) {
return env.open_window.apply(rcmail, arguments);
}
// _extwin=1, _framed=1 are required to display attachment preview
// layout properly and make mobile menus working
url = rcmail.add_url(url, '_framed', 1);
url = rcmail.add_url(url, '_extwin', 1);
var label, title = '',
props = {cancel_button: 'close', width: 768, height: 768},
frame = $('<iframe>').attr({id: 'windowframe', src: url});
if (/_action=([a-z_]+)/.test(url) && (label = rcmail.labels[RegExp.$1])) {
title = label;
}
if (/_frame=1/.test(url)) {
props.dialogClass = 'no-titlebar';
}
rcmail.simple_dialog(frame, title, null, props);
return true;
};
/**
* Get layout modes. In frame mode returns the parent layout modes.
*/
function layout_metadata()
{
if (is_framed) {
var doc = $(parent.document.documentElement);
return {
mode: doc[0].className.match(/layout-([a-z]+)/) ? RegExp.$1 : mode,
touch: doc.is('.touch'),
};
}
return {mode: mode, touch: touch};
};
/**
* Returns true if the layout is in 'small' or 'phone' mode
*/
function is_mobile()
{
var meta = layout_metadata();
return meta.mode == 'phone' || meta.mode == 'small';
};
/**
* Returns true if the layout is in 'touch' mode
*/
function is_touch()
{
var meta = layout_metadata();
return meta.touch;
};
}
if (window.rcmail) {
/**
* Elastic version of show_menu as we don't need e.g. menu positioning from core
* TODO: keyboard navigation in menus
*/
rcmail.show_menu = function(prop, show, event)
{
var name = typeof prop == 'object' ? prop.menu : prop,
obj = $('#' + name);
if (typeof prop == 'string') {
prop = {menu: name};
}
// just delegate the action to rcube_elastic_ui
return rcmail.triggerEvent(show === false ? 'menu-close' : 'menu-open', {name: name, obj: obj, props: prop, originalEvent: event});
}
/**
* Elastic version of hide_menu as we don't need e.g. menus stack handling
*/
rcmail.hide_menu = function(name, event)
{
// delegate to rcube_elastic_ui
return rcmail.triggerEvent('menu-close', {name: name, props: {menu: name}, originalEvent: event});
}
}
else {
// rcmail does not exists e.g. on the error template inside a frame
// we fake the engine a little
var rcmail = parent.rcmail;
var rcube_webmail = parent.rcube_webmail;
var bw = {};
}
var UI = new rcube_elastic_ui();

File Metadata

Mime Type
text/x-diff
Expires
Sat, Apr 18, 10:05 AM (2 h, 52 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
435636
Default Alt Text
(162 KB)

Event Timeline