From c6447e2ce289188493590ec0d5449fa3692eed08 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Wed, 02 Jul 2014 07:03:22 -0400 Subject: [PATCH] Use treelist widget for folders list in Settings/Folders (#1489648) --- program/js/app.js | 320 +++++++++++++++++++++++++++-------------------------- 1 files changed, 164 insertions(+), 156 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index 9c2acfc..2b9c3f0 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -46,12 +46,12 @@ this.messages = {}; this.group2expand = {}; this.http_request_jobs = {}; - this.menu_stack = new Array(); + this.menu_stack = []; // webmail client settings this.dblclick_time = 500; this.message_time = 5000; - this.identifier_expr = new RegExp('[^0-9a-z\-_]', 'gi'); + this.identifier_expr = /[^0-9a-z_-]/gi; // environment defaults this.env = { @@ -314,9 +314,9 @@ }); // avoid textarea loosing focus when hitting the save-response button/link - for (var i=0; this.buttons['save-response'] && i < this.buttons['save-response'].length; i++) { - $('#'+this.buttons['save-response'][i].id).mousedown(function(e){ return rcube_event.cancel(e); }) - } + $.each(this.buttons['save-response'] || [], function (i, v) { + $('#' + v.id).mousedown(function(e){ return rcube_event.cancel(e); }) + }); } // init message compose form @@ -346,10 +346,10 @@ this.contact_list .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); }) .addEventListener('select', function(o) { ref.compose_recipient_select(o); }) - .addEventListener('dblclick', function(o) { ref.compose_add_recipient('to'); }) + .addEventListener('dblclick', function(o) { ref.compose_add_recipient(); }) .addEventListener('keypress', function(o) { if (o.key_pressed == o.ENTER_KEY) { - if (!ref.compose_add_recipient('to')) { + if (!ref.compose_add_recipient()) { // execute link action on <enter> if not a recipient entry if (o.last_selected && String(o.last_selected).charAt(0) == 'G') { $(o.rows[o.last_selected].obj).find('a').first().click(); @@ -358,6 +358,9 @@ } }) .init(); + + // remember last focused address field + $('#_to,#_cc,#_bcc').focus(function() { ref.env.focused_field = this; }); } if (this.gui_objects.addressbookslist) { @@ -403,15 +406,23 @@ .addEventListener('dragend', function(e) { ref.drag_end(e); }) .init(); - if (this.env.cid) - this.contact_list.highlight_row(this.env.cid); - this.gui_objects.contactslist.parentNode.onmousedown = function(e){ return ref.click_on_list(e); }; $(this.gui_objects.qsearchbox).focusin(function() { ref.contact_list.blur(); }); this.update_group_commands(); this.command('list'); + } + + if (this.gui_objects.savedsearchlist) { + this.savedsearchlist = new rcube_treelist_widget(this.gui_objects.savedsearchlist, { + id_prefix: 'rcmli', + id_encode: this.html_identifier_encode, + id_decode: this.html_identifier_decode + }); + + this.savedsearchlist.addEventListener('select', function(node) { + ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }); }); } this.set_page_buttons(); @@ -471,9 +482,6 @@ }) .init() .focus(); - - if (this.env.iid) - this.identity_list.highlight_row(this.env.iid); } else if (this.gui_objects.sectionslist) { this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:true}); @@ -557,6 +565,7 @@ // init treelist widget if (this.gui_objects.folderlist && window.rcube_treelist_widget) { this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, { + selectable: true, id_prefix: 'rcmli', id_encode: this.html_identifier_encode, id_decode: this.html_identifier_decode, @@ -621,7 +630,7 @@ { var ret, uid, cid, url, flag, aborted = false; - if (obj && obj.blur && !(event || rcube_event.is_keyboard(event))) + if (obj && obj.blur && !(event && rcube_event.is_keyboard(event))) obj.blur(); // do nothing if interface is locked by other command (with exception for searching reset) @@ -1344,7 +1353,7 @@ this.command_enabled = function(cmd) { return this.commands[cmd]; - } + }; // lock/unlock interface this.set_busy = function(a, message, id) @@ -1386,10 +1395,11 @@ // switch to another application task this.switch_task = function(task) { - if (this.task===task && task!='mail') + if (this.task === task && task != 'mail') return; var url = this.get_task_url(task); + if (task == 'mail') url += '&_mbox=INBOX'; else if (task == 'logout') @@ -1444,10 +1454,10 @@ this.save_pref = function(prop) { - var request = {'_name': prop.name, '_value': prop.value}; + var request = {_name: prop.name, _value: prop.value}; if (prop.session) - request['_session'] = prop.session; + request._session = prop.session; if (prop.env) this.env[prop.env] = prop.value; @@ -1587,7 +1597,7 @@ // select the folder if one of its childs is currently selected // don't select if it's virtual (#1488346) - if (this.env.mailbox && this.env.mailbox.startsWith(name + this.env.delimiter) && !node.virtual) + if (!node.virtual && this.env.mailbox && this.env.mailbox.startsWith(name + this.env.delimiter)) this.command('list', name); } else { @@ -1629,7 +1639,7 @@ } // reset popup menus; delayed to have updated menu_stack data - window.setTimeout(function(e){ + setTimeout(function(e){ var obj, skip, config, id, i, parents = $(target).parents(); for (i = ref.menu_stack.length - 1; i >= 0; i--) { id = ref.menu_stack[i]; @@ -1681,8 +1691,8 @@ case 40: case 63232: // "up", in safari keypress case 63233: // "down", in safari keypress - focus_menu_item(mod = keyCode == 38 || keyCode == 63232 ? -1 : 1); - break; + focus_menu_item(keyCode == 38 || keyCode == 63232 ? -1 : 1); + return rcube_event.cancel(e); case 9: // tab if (this.focused_menu) { @@ -1845,9 +1855,6 @@ return (this.env.mailboxes[id] && !this.env.mailboxes[id].virtual && (this.env.mailboxes[id].id != this.env.mailbox || this.is_multifolder_listing())) ? 1 : 0; - - case 'settings': - return id != this.env.mailbox ? 1 : 0; case 'addressbook': var target; @@ -2480,7 +2487,7 @@ selection.push(selected[i]); this.message_list.selection = selection; - } + }; // expand all threads with unread children this.expand_unread = function() @@ -2493,8 +2500,10 @@ this.message_list.expand_all(r); this.set_unread_children(r.uid); } + new_row = new_row.nextSibling; } + return false; }; @@ -2711,8 +2720,8 @@ } // update unread_children for roots - for (var i=0; i<roots.length; i++) - this.set_unread_children(roots[i].uid); + for (r=0; r<roots.length; r++) + this.set_unread_children(roots[r].uid); return count; }; @@ -3488,6 +3497,12 @@ this.compose_add_recipient = function(field) { + // find last focused field name + if (!field) { + field = $(this.env.focused_field).filter(':visible'); + field = field.length ? field.attr('id').replace('_', '') : 'to'; + } + var recipients = [], input = $('#_'+field), delim = this.env.recipients_delimiter; if (this.contact_list && this.contact_list.selection.length) { @@ -3556,26 +3571,32 @@ myprompt = $('<div class="prompt">').html('<div class="message">' + this.get_label('nosubjectwarning') + '</div>') .appendTo(document.body), prompt_value = $('<input>').attr({type: 'text', size: 30}).val(this.get_label('nosubject')) - .appendTo(myprompt); + .appendTo(myprompt), + save_func = function() { + input_subject.val(prompt_value.val()); + myprompt.dialog('close'); + ref.command(cmd, { nocheck:true }); // repeat command which triggered this + }; - buttons[this.get_label('cancel')] = function(){ + buttons[this.get_label('sendmessage')] = function() { + save_func($(this)); + }; + buttons[this.get_label('cancel')] = function() { input_subject.focus(); $(this).dialog('close'); - }; - buttons[this.get_label('sendmessage')] = function(){ - input_subject.val(prompt_value.val()); - $(this).dialog('close'); - ref.command(cmd, { nocheck:true }); // repeat command which triggered this }; myprompt.dialog({ modal: true, resizable: false, buttons: buttons, - close: function(event, ui) { $(this).remove() } + close: function(event, ui) { $(this).remove(); } }); - prompt_value.select(); + prompt_value.select().keydown(function(e) { + if (e.which == 13) save_func(); + }); + return false; } @@ -3653,7 +3674,7 @@ $(this).dialog('close'); }; - this.show_popup_dialog(html, this.gettext('savenewresponse'), buttons); + this.show_popup_dialog(html, this.gettext('newresponse'), buttons); $('#ffresponsetext').val(text); $('#ffresponsename').select(); @@ -3712,8 +3733,9 @@ { var active = this.editor.spellcheck_state(); - if (this.buttons.spellcheck) - $('#'+this.buttons.spellcheck[0].id)[active ? 'addClass' : 'removeClass']('selected'); + $.each(this.buttons.spellcheck || [], function(i, v) { + $('#' + v.id)[active ? 'addClass' : 'removeClass']('selected'); + }); return active; }; @@ -3911,7 +3933,7 @@ } this.local_storage_remove_item('compose.index'); } - } + }; this.change_identity = function(obj, show_sig) @@ -4058,6 +4080,14 @@ if (upload_id) this.triggerEvent('fileuploaded', {name: name, attachment: att, id: upload_id}); + if (!this.env.attachments) + this.env.attachments = {}; + + if (upload_id && this.env.attachments[upload_id]) + delete this.env.attachments[upload_id]; + + this.env.attachments[name] = att; + if (!this.gui_objects.attachmentlist) return false; @@ -4066,7 +4096,7 @@ if (!att.complete && att.frame) att.html = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\', \''+att.frame+'\');" href="#cancelupload" class="cancelupload">' - + (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="" />' : this.get_label('cancel')) + '</a>' + att.html; + + (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="'+this.get_label('cancel')+'" />' : this.get_label('cancel')) + '</a>' + att.html; var indicator, li = $('<li>'); @@ -4083,10 +4113,9 @@ li.appendTo(this.gui_objects.attachmentlist); } - if (upload_id && this.env.attachments[upload_id]) - delete this.env.attachments[upload_id]; - - this.env.attachments[name] = att; + // set tabindex attribute + var tabindex = $(this.gui_objects.attachmentlist).attr('data-tabindex') || '0'; + li.find('a').attr('tabindex', tabindex); return true; }; @@ -4125,7 +4154,7 @@ this.upload_progress_update = function(param) { - var elem = $('#'+param.name + '> span'); + var elem = $('#'+param.name + ' > span'); if (!elem.length || !param.text) return; @@ -4282,21 +4311,22 @@ this.display_message(msg, type); if (this.env.extwin) { - var rc = this.opener(); this.lock_form(this.gui_objects.messageform); + + var rc = this.opener(); if (rc) { rc.display_message(msg, type); // refresh the folder where sent message was saved or replied message comes from if (folders && rc.env.task == 'mail' && rc.env.action == '' && $.inArray(rc.env.mailbox, folders) >= 0) { - // @TODO: try with 'checkmail' here when #1485186 is fixed. See also #1489249. - rc.command('list'); + rc.command('checkmail'); } } - setTimeout(function(){ window.close() }, 1000); + + setTimeout(function() { window.close(); }, 1000); } else { // before redirect we need to wait some time for Chrome (#1486177) - setTimeout(function(){ ref.list_mailbox(); }, 500); + setTimeout(function() { ref.list_mailbox(); }, 500); } }; @@ -4519,7 +4549,7 @@ // register (delegate) event handlers ul.on('mouseover', 'li', function(e){ ref.ksearch_select(e.target); }) - .on('onmouseup', 'li', function(e){ ref.ksearch_click(e.target); }) + .on('mouseup', 'li', function(e){ ref.ksearch_click(e.target); }) } ul = this.ksearch_pane.__ul; @@ -4742,7 +4772,8 @@ $(this.gui_objects.addresslist_title).html(this.get_label('contacts')); } - this.select_folder(folder, '', true); + if (!this.env.search_id) + this.select_folder(folder, '', true); // load contacts remotely if (this.gui_objects.contactslist) { @@ -4974,7 +5005,7 @@ { var selection = this.contact_list ? this.contact_list.get_selection() : []; - // exit if no mailbox specified or if selection is empty + // exit if no contact specified or if selection is empty if (!selection.length && !this.env.cid) return; @@ -5166,7 +5197,7 @@ // find list (UL) element if (type == 'contactsearch') - ul = this.gui_objects.folderlist; + ul = this.gui_objects.savedsearchlist; else ul = $('ul.groups', this.get_folder_li(this.env.source,'',true)); @@ -5267,7 +5298,7 @@ .html(prop.name); this.env.contactfolders[key] = this.env.contactgroups[key] = prop; - this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, true); + this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, 'contactgroup'); this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key) }); }; @@ -5549,7 +5580,7 @@ .html(name), prop = { name:name, id:id }; - this.treelist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch'); + this.savedsearchlist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch'); this.select_folder(key,'',true); this.enable_command('search-delete', true); this.env.search_id = id; @@ -5575,7 +5606,7 @@ this.remove_search_item = function(id) { var li, key = 'S'+id; - if (this.treelist.remove(key)) { + if (this.savedsearchlist.remove(key)) { this.triggerEvent('search_delete', { id:id, li:li }); } @@ -5595,7 +5626,13 @@ } this.reset_qsearch(); - this.select_folder('S'+id, '', true); + + if (this.savedsearchlist) { + this.treelist.select(''); + this.savedsearchlist.select('S'+id); + } + else + this.select_folder('S'+id, '', true); // reset vars this.env.current_page = 1; @@ -5725,62 +5762,38 @@ this.last_sub_rx = RegExp('['+delim+']?[^'+delim+']+$'); - this.subscription_list = new rcube_list_widget(this.gui_objects.subscriptionlist, - {multiselect:false, draggable:true, keyboard:true, toggleselect:true}); + this.subscription_list = new rcube_treelist_widget(this.gui_objects.subscriptionlist, { + selectable: true + }); + this.subscription_list - .addEventListener('select', function(o){ ref.subscription_select(o); }) - .addEventListener('dragstart', function(o){ ref.drag_active = true; }) - .addEventListener('dragend', function(o){ ref.subscription_move_folder(o); }) - .addEventListener('initrow', function (row) { - row.obj.onmouseover = function() { ref.focus_subscription(row.id); }; - row.obj.onmouseout = function() { ref.unfocus_subscription(row.id); }; - }) - .init() - .focus(); + .addEventListener('select', function(node) { ref.subscription_select(node.id); }) + .draggable({cancel: '#mailboxroot'}) + .droppable({ + // @todo: find better way, accept callback is executed for every folder + // on the list when dragging starts (and stops), this is slow, but + // I didn't find a method to check droptarget on over event + accept: function(node) { + var source = ref.env.subscriptionrows[$(node).attr('id')], + dest = ref.env.subscriptionrows[this.id], + source_name = source[0], + dest_name = dest[0]; - $('#mailboxroot') - .mouseover(function(){ ref.focus_subscription(this.id); }) - .mouseout(function(){ ref.unfocus_subscription(this.id); }) - }; - - this.focus_subscription = function(id) - { - var row, folder; - - if (this.drag_active && this.env.mailbox && (row = document.getElementById(id))) - if (this.env.subscriptionrows[id] && - (folder = this.env.subscriptionrows[id][0]) !== null - ) { - if (this.check_droptarget(folder) && - !this.env.subscriptionrows[this.get_folder_row_id(this.env.mailbox)][2] && - folder != this.env.mailbox.replace(this.last_sub_rx, '') && - !folder.startsWith(this.env.mailbox + this.env.delimiter) - ) { - this.env.dstfolder = folder; - $(row).addClass('droptarget'); + return !source[2] + && dest_name != source_name.replace(ref.last_sub_rx, '') + && !dest_name.startsWith(source_name + ref.env.delimiter); + }, + drop: function(e, ui) { + ref.subscription_move_folder(ui.draggable.attr('id'), this.id); } - } + }); }; - this.unfocus_subscription = function(id) + this.subscription_select = function(id) { - var row = $('#'+id); + var folder; - this.env.dstfolder = null; - - if (row.length && this.env.subscriptionrows[id]) - row.removeClass('droptarget'); - else - $(this.subscription_list.frame).removeClass('droptarget'); - }; - - this.subscription_select = function(list) - { - var id, folder; - - if (list && (id = list.get_single_selection()) && - (folder = this.env.subscriptionrows['rcmrow'+id]) - ) { + if (id && id != 'mailboxroot' && (folder = this.env.subscriptionrows[id])) { this.env.mailbox = folder[0]; this.show_folder(folder[0]); this.enable_command('delete-folder', !folder[2]); @@ -5792,24 +5805,21 @@ } }; - this.subscription_move_folder = function(list) + this.subscription_move_folder = function(from, to) { - if (this.env.mailbox && this.env.dstfolder !== null && - this.env.dstfolder != this.env.mailbox && - this.env.dstfolder != this.env.mailbox.replace(this.last_sub_rx, '') - ) { - var path = this.env.mailbox.split(this.env.delimiter), - basename = path.pop(), - newname = this.env.dstfolder === '' ? basename : this.env.dstfolder + this.env.delimiter + basename; + var source = this.env.subscriptionrows[from][0]; + dest = this.env.subscriptionrows[to][0]; - if (newname != this.env.mailbox) { - this.http_post('rename-folder', {_folder_oldname: this.env.mailbox, _folder_newname: newname}, this.set_busy(true, 'foldermoving')); - this.subscription_list.draglayer.hide(); + if (source && dest !== null && source != dest && dest != source.replace(this.last_sub_rx, '')) { + var path = source.split(this.env.delimiter), + basename = path.pop(), + newname = dest === '' ? basename : dest + this.env.delimiter + basename; + + if (newname != source) { + this.http_post('rename-folder', {_folder_oldname: source, _folder_newname: newname}, + this.set_busy(true, 'foldermoving')); } } - - this.drag_active = false; - this.unfocus_subscription(this.get_folder_row_id(this.env.dstfolder)); }; // tell server to create and subscribe a new mailbox @@ -5825,8 +5835,7 @@ folder = this.env.subscriptionrows[id][0]; if (folder && confirm(this.get_label('deletefolderconfirm'))) { - var lock = this.set_busy(true, 'folderdeleting'); - this.http_post('delete-folder', {_mbox: folder}, lock); + this.http_post('delete-folder', {_mbox: folder}, this.set_busy(true, 'folderdeleting')); } }; @@ -5838,9 +5847,9 @@ var row, n, tmp, tmp_name, rowid, collator, folders = [], list = [], slist = [], - tbody = this.gui_objects.subscriptionlist.tBodies[0], - refrow = $('tr', tbody).get(1), - id = 'rcmrow'+((new Date).getTime()); + list_element = $(this.gui_objects.subscriptionlist), + refrow = $('li', list_element).get(1), + id = 'rcmli'+((new Date).getTime()); if (!refrow) { // Refresh page if we don't have a table row to clone @@ -5855,7 +5864,7 @@ row.attr({id: id, 'class': class_name}); // set folder name - row.find('td:first').html(display_name); + $('.name', row).html(display_name); // update subscription checkbox $('input[name="_subscribed[]"]', row).val(name) @@ -5926,12 +5935,13 @@ // add row to the table if (rowid) - $('#'+rowid).after(row); + $('#' + rowid).after(row); else - row.appendTo(tbody); + list_element.append(row); // update list widget - this.subscription_list.clear_selection(); + this.subscription_list.select(); + if (!skip_init) this.init_subscription_list(); @@ -5948,11 +5958,11 @@ if (!this.gui_objects.subscriptionlist) { if (this.is_framed) return parent.rcmail.replace_folder_row(oldfolder, newfolder, display_name, is_protected, class_name); + return false; } var i, n, len, name, dispname, oldrow, tmprow, row, level, - tbody = this.gui_objects.subscriptionlist.tBodies[0], folders = this.env.subscriptionrows, id = this.get_folder_row_id(oldfolder), prefix_len = oldfolder.length, @@ -5963,7 +5973,6 @@ // no renaming, only update class_name if (oldfolder == newfolder) { $('#'+id).attr('class', class_name || ''); - this.subscription_list.focus(); return; } @@ -6000,7 +6009,7 @@ for (i=level; i<0; i++) dispname = ' ' + dispname; } - row.find('td:first').html(dispname); + $('.name', row).html(dispname); this.env.subscriptionrows[id][1] = dispname; } } @@ -6028,8 +6037,8 @@ this._remove_folder_row = function(id) { - this.subscription_list.remove_row(id.replace(/^rcmrow/, '')); - $('#'+id).remove(); + this.subscription_list.remove(id.replace(/^rcmli/, '')); + $('#' + id).remove(); delete this.env.subscriptionrows[id]; }; @@ -6460,6 +6469,10 @@ // mark a mailbox as selected and set environment variable this.select_folder = function(name, prefix, encode) { + if (this.savedsearchlist) { + this.savedsearchlist.select(''); + } + if (this.treelist) { this.treelist.select(name); } @@ -7499,8 +7512,10 @@ // post the given form to a hidden iframe this.async_upload_form = function(form, action, onload) { - var frame, ts = new Date().getTime(), - frame_name = 'rcmupload'+ts; + // create hidden iframe + var ts = new Date().getTime(), + frame_name = 'rcmupload' + ts, + frame = this.async_upload_form_frame(frame_name); // upload progress support if (this.env.upload_progress_name) { @@ -7515,31 +7530,24 @@ field.val(ts); } - // have to do it this way for IE - // otherwise the form will be posted to a new window - if (document.all) { - document.body.insertAdjacentHTML('BeforeEnd', '<iframe name="'+frame_name+'"' - + ' src="program/resources/blank.gif" style="width:0;height:0;visibility:hidden;"></iframe>'); - frame = $('iframe[name="'+frame_name+'"]'); - } - // for standards-compliant browsers - else { - frame = $('<iframe>').attr('name', frame_name) - .css({border: 'none', width: 0, height: 0, visibility: 'hidden'}) - .appendTo(document.body); - } - - // handle upload errors, parsing iframe content in onload + // handle upload errors by parsing iframe content in onload frame.bind('load', {ts:ts}, onload); $(form).attr({ target: frame_name, - action: this.url(action, { _id:this.env.compose_id||'', _uploadid:ts }), + action: this.url(action, {_id: this.env.compose_id || '', _uploadid: ts, _from: this.env.action}), method: 'POST'}) .attr(form.encoding ? 'encoding' : 'enctype', 'multipart/form-data') .submit(); return frame_name; + }; + + // create iframe element for files upload + this.async_upload_form_frame = function(name) + { + return $('<iframe>').attr({name: name, style: 'border: none; width: 0; height: 0; visibility: hidden'}) + .appendTo(document.body); }; // html5 file-drop API @@ -7590,7 +7598,7 @@ $.ajax({ type: 'POST', dataType: 'json', - url: ref.url(ref.env.filedrop.action||'upload', { _id:ref.env.compose_id||ref.env.cid||'', _uploadid:ts, _remote:1 }), + url: ref.url(ref.env.filedrop.action || 'upload', {_id: ref.env.compose_id||ref.env.cid||'', _uploadid: ts, _remote: 1, _from: ref.env.action}), contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary, processData: false, timeout: 0, // disable default timeout set in ajaxSetup() -- Gitblit v1.9.1