Move sortable folder list code to treelist.js and adapt to previous UX

This commit is contained in:
Pablo Zmdl
2025-12-09 19:14:01 +01:00
parent 5cb808be51
commit 206c37afe7
2 changed files with 144 additions and 94 deletions

View File

@@ -774,7 +774,7 @@ function rcube_webmail() {
var body_mouseup = function (e) {
// Stop dragging in sortable list if the mouseup event happens over an iframe.
if (ref.gui_objects.subscriptionlist && e.target.ownerDocument !== ref.gui_objects.subscriptionlist.ownerDocument) {
$(ref.gui_objects.subscriptionlist).trigger('mouseup');
ref.subscription_list.sortable_cancel();
}
return ref.doc_mouse_up(e);
};
@@ -7802,6 +7802,7 @@ function rcube_webmail() {
});
this.subscription_list
.sortable_init()
.addEventListener('select', function (node) {
ref.subscription_select(node.id);
})
@@ -7816,100 +7817,15 @@ function rcube_webmail() {
ref.subscription_select();
}
});
this.make_folder_lists_sortable();
};
// TODO: In the receive callback, can we wait for the confirmation dialog without introducing async/await and Promises?
this.make_folder_lists_sortable = () => {
const mainFolderList = this.gui_objects.subscriptionlist;
$folderLists = $('ul', mainFolderList.parentElement);
$folderLists.sortable({
axis: 'y',
// We can't use `li.mailbox.protected` here because that would disable moving elements out of protected
// folders. jQuery UI uses `closest()` with this selector, which makes it impossible to keep main list items
// and sub-list items apart. We disable sorting protected items via a `mousedown` event in treelist.js.
cancel: 'input, div.treetoggle, .custom-control',
helper: 'clone', // a clone doesn't have the borders, which looks nicer.
items: '> li.mailbox', // handle only the directly descending items, not those of sub-lists (they get they own instance of $.sortable()
connectWith: `#${mainFolderList.id}, #${mainFolderList.id} ul`,
forcePlaceholderSize: true, // Make the placeholder displace the neighboring elements.
placeholder: 'placeholder', // Class name for the placeholder
change: (event, ui) => {
// Prevent sortable folders being sorted in between (technically: before) protected folders. There is no
// technical reason for this, we just want it from a UX perspective.
if (ui.placeholder.next().is('.protected')) {
ui.placeholder.hide();
} else {
ui.placeholder.show();
}
},
over: (event, ui) => {
// Highlight the list that the dragged element is hovering over.
$('.hover', $folderLists).removeClass('hover');
if (event.target !== mainFolderList) {
$(event.target).closest('li').addClass('hover');
}
},
receive: async (event, ui) => {
$('.hover', $folderLists).removeClass('hover');
const folderId = ui.item.attr('id');
const folderName = ref.folder_id2name(folderId);
const folderAttribs = ref.env.subscriptionrows[folderName];
let destName;
if (event.target === mainFolderList) {
destName = '*';
} else {
const destId = event.target.parentElement.id;
destName = ref.folder_id2name(destId);
}
if (!(
folderAttribs && !folderAttribs[2]
&& destName != folderName.replace(ref.last_sub_rx, '')
&& !destName.startsWith(folderName + ref.env.delimiter)
)) {
ui.sender.sortable('cancel');
}
const result = await ref.subscription_move_folder(folderName, destName);
if (!result) {
ui.sender.sortable('cancel');
}
},
stop: (event, ui) => {
$('.hover', $folderLists).removeClass('hover');
if (ui.item.next().is('.protected')) {
ui.item.parent().sortable('cancel');
return false;
}
},
update: (event, ui) => {
// Save the order if the item was moved only within its list. In case it was moved into a (different)
// sub-list, the order-saving function gets called from the server's response after the relevant folder
// rows have been re-rendered, and we can save one HTTP request. We don't skip the other function call
// because in this moment here we don't know yet if the confirmation dialog about moving the folder will
// be confirmed or cancelled.
if (ui.item[0].parentElement === event.target) {
ref.save_reordered_folder_list();
}
},
});
};
this.save_reordered_folder_list = () => {
const mainList = ref.subscription_list.container.sortable('toArray');
const subLists = ref.subscription_list.container.find('.ui-sortable').map((i, elem) => ({
parentId: elem.parentElement.id,
elems: $(elem).sortable('toArray'),
})).toArray();
// Sort sub-lists after their their parent element, so the sorting for the settings page doesn't get confused
// (which will hook child-folders onto wrong parents if we don't do this).
subLists.forEach((subList) => {
mainList.splice(mainList.indexOf(subList.parentId) + 1, 0, ...subList.elems);
});
params = mainList.map((e) => e.replace(/^rcmli/, 'folderorder[]=')).join('&');
const items = ref.subscription_list.sortable_get_items();
if (!items) {
console.error('Failed to get sorted items from folder list, cannot save.');
return false;
}
const params = items.map((e) => e.replace(/^rcmli/, 'folderorder[]=')).join('&');
this.http_post('folder-reorder', params, this.display_message('', 'loading'));
};
@@ -7999,8 +7915,18 @@ function rcube_webmail() {
}
};
this.subscription_move_folder = function (from, to) {
if (from && to !== null && from != to && to != from.replace(this.last_sub_rx, '')) {
this.subscription_move_folder = function (folderId, destId) {
const from = rcmail.folder_id2name(folderId);
const fromAttribs = rcmail.env.subscriptionrows[from];
let to;
if (destId === '*') {
to = '*';
} else {
to = rcmail.folder_id2name(destId);
}
if (from && fromAttribs && !fromAttribs[2] && to !== null && !to.startsWith(from + rcmail.env.delimiter) && from != to && to != from.replace(this.last_sub_rx, '')) {
var path = from.split(this.env.delimiter),
basename = path.pop(),
newname = to === '' || to === '*' ? basename : to + this.env.delimiter + basename;

View File

@@ -106,6 +106,9 @@ function rcube_treelist_widget(node, p) {
this.get_single_selection = get_selection;
this.is_search = is_search;
this.reset_search = reset_search;
this.sortable_init = sortable_init;
this.sortable_cancel = sortable_cancel;
this.sortable_get_items = sortable_get_items;
// ///// startup code (constructor)
@@ -1328,6 +1331,127 @@ function rcube_treelist_widget(node, p) {
function is_draggable() {
return !!ui_draggable;
}
function sortable_init() {
const mainList = node;
me.sortable_lists = $(mainList.parentElement).find('ul');
const listContainerElem = $(mainList).parents('#layout-list')[0];
me.sortable_lists.sortable({
// We can't use `li.mailbox.protected` here because that would disable moving elements out of protected
// folders. jQuery UI uses `closest()` with this selector, which makes it impossible to keep main list items
// and sub-list items apart. We disable sorting protected items via a `mousedown` event in the
// setup code of this rcube_treelist_widget().
cancel: 'input, div.treetoggle, .custom-control',
// Use a custom helper element so we can style it.
// helper: 'original',
helper: (event, item) => $('<div>').attr('id', 'rcmdraglayer').text(item.children('a').text().trim()),
appendTo: 'body', // append the helper element to the body so we can make it float outside the lists.
cursor: 'pointer',
cursorAt: { top: 0, left: -20 }, // place the helper element to the right of the mouse pointer.
tolerance: 'pointer', // Position the placeholder according to the mouse pointer, not the helper element.
items: '> li.mailbox', // handle only the directly descending items, not those of sub-lists (they get they own instance of $.sortable()
connectWith: `#${mainList.id}, #${mainList.id} ul`,
placeholder: 'placeholder', // Class name for the placeholder
start: (event, ui) => {
// We need the `ui` in the callback and can't pass it as function argument if the callback is
// called via `body_mouseup()`, so we store a reference.
me.sortable_ui = ui;
me.sortable_mouse_move_handler = (event) => {
const lastElementUnderCursor = document.elementFromPoint(event.pageX, event.pageY);
// Cancel the sorting if the mouse is dragged out of the list "column".
if ($.contains(listContainerElem, lastElementUnderCursor) === false) {
me.sortable_cancel();
}
};
document.addEventListener('mousemove', me.sortable_mouse_move_handler);
},
stop: (event, ui) => {
// Reset some states.
document.removeEventListener('mousemove', me.sortable_mouse_move_handler);
me.sortable_lists.find('.hover').removeClass('hover');
me.sortable_cancelling = false;
},
change: (event, ui) => {
// Prevent sortable folders being sorted in between (technically: before) protected folders.
// There is no technical reason for this, we just want it from a UX perspective.
if (ui.placeholder.next().is('.protected')) {
ui.placeholder.hide();
} else {
ui.placeholder.show();
}
},
over: (event, ui) => {
// Highlight only the list that the dragged element is hovering over.
me.sortable_lists.find('.hover').removeClass('hover');
if (event.target !== mainList) {
$(event.target).closest('li').addClass('hover');
}
},
// We have to make this async so we can wait for the confirmation dialog.
receive: async (event, ui) => {
me.sortable_lists.find('.hover').removeClass('hover');
if (me.sortable_cancelling) {
// Don't do anything, the sorting is cancelled.
return;
}
let destId;
if (event.target === mainList) {
destId = '*';
} else {
destId = event.target.parentElement.id;
}
const result = await rcmail.subscription_move_folder(ui.item.attr('id'), destId);
if (!result) {
ui.sender.sortable('cancel');
}
},
update: (event, ui) => {
me.sortable_lists.find('.hover').removeClass('hover');
if (me.sortable_cancelling) {
// Don't do anything, the sorting is cancelled.
return;
}
if (ui.item.next().is('.protected')) {
ui.item.parent().sortable('cancel');
return false;
}
// Save the order if the item was moved only within its list. In case it was moved into a (different)
// sub-list, the order-saving function gets called from the server's response after the relevant folder
// rows have been re-rendered, and we can save one HTTP request. We don't skip the other function call
// because in this moment here we don't know yet if the confirmation dialog about moving the folder will
// be confirmed or cancelled.
if (ui.item[0].parentElement === event.target) {
rcmail.save_reordered_folder_list();
}
},
});
return me;
}
function sortable_cancel() {
me.sortable_lists.find('.hover').removeClass('hover');
me.sortable_cancelling = true;
me.sortable_ui.placeholder.hide();
me.sortable_ui.helper.hide({ duration: 300 });
me.sortable_ui.item.show({ duration: 300 });
setTimeout(() => me.sortable_lists.sortable('cancel'), 400);
}
function sortable_get_items() {
const items = me.container.sortable('toArray');
const subLists = me.container.find('.ui-sortable').map((i, elem) => ({
parentId: elem.parentElement.id,
elems: $(elem).sortable('toArray'),
})).toArray();
// Sort sub-lists after their their parent element, so the sorting for the settings page doesn't get confused
// (which will hook child-folders onto wrong parents if we don't do this).
subLists.forEach((subList) => {
items.splice(items.indexOf(subList.parentId) + 1, 0, ...subList.elems);
});
return items;
}
}
// use event processing functions from Roundcube's rcube_event_engine