Skip to content

Commit a515cf7

Browse files
committed
Move sortable folder list code to treelist.js and adapt to previous UX
1 parent 77d94cd commit a515cf7

File tree

2 files changed

+144
-94
lines changed

2 files changed

+144
-94
lines changed

program/js/app.js

Lines changed: 20 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ function rcube_webmail() {
774774
var body_mouseup = function (e) {
775775
// Stop dragging in sortable list if the mouseup event happens over an iframe.
776776
if (ref.gui_objects.subscriptionlist && e.target.ownerDocument !== ref.gui_objects.subscriptionlist.ownerDocument) {
777-
$(ref.gui_objects.subscriptionlist).trigger('mouseup');
777+
ref.subscription_list.sortable_cancel();
778778
}
779779
return ref.doc_mouse_up(e);
780780
};
@@ -7802,6 +7802,7 @@ function rcube_webmail() {
78027802
});
78037803

78047804
this.subscription_list
7805+
.sortable_init()
78057806
.addEventListener('select', function (node) {
78067807
ref.subscription_select(node.id);
78077808
})
@@ -7816,100 +7817,15 @@ function rcube_webmail() {
78167817
ref.subscription_select();
78177818
}
78187819
});
7819-
7820-
this.make_folder_lists_sortable();
7821-
};
7822-
7823-
// TODO: In the receive callback, can we wait for the confirmation dialog without introducing async/await and Promises?
7824-
this.make_folder_lists_sortable = () => {
7825-
const mainFolderList = this.gui_objects.subscriptionlist;
7826-
$folderLists = $('ul', mainFolderList.parentElement);
7827-
$folderLists.sortable({
7828-
axis: 'y',
7829-
// We can't use `li.mailbox.protected` here because that would disable moving elements out of protected
7830-
// folders. jQuery UI uses `closest()` with this selector, which makes it impossible to keep main list items
7831-
// and sub-list items apart. We disable sorting protected items via a `mousedown` event in treelist.js.
7832-
cancel: 'input, div.treetoggle, .custom-control',
7833-
helper: 'clone', // a clone doesn't have the borders, which looks nicer.
7834-
items: '> li.mailbox', // handle only the directly descending items, not those of sub-lists (they get they own instance of $.sortable()
7835-
connectWith: `#${mainFolderList.id}, #${mainFolderList.id} ul`,
7836-
forcePlaceholderSize: true, // Make the placeholder displace the neighboring elements.
7837-
placeholder: 'placeholder', // Class name for the placeholder
7838-
change: (event, ui) => {
7839-
// Prevent sortable folders being sorted in between (technically: before) protected folders. There is no
7840-
// technical reason for this, we just want it from a UX perspective.
7841-
if (ui.placeholder.next().is('.protected')) {
7842-
ui.placeholder.hide();
7843-
} else {
7844-
ui.placeholder.show();
7845-
}
7846-
},
7847-
over: (event, ui) => {
7848-
// Highlight the list that the dragged element is hovering over.
7849-
$('.hover', $folderLists).removeClass('hover');
7850-
if (event.target !== mainFolderList) {
7851-
$(event.target).closest('li').addClass('hover');
7852-
}
7853-
},
7854-
receive: async (event, ui) => {
7855-
$('.hover', $folderLists).removeClass('hover');
7856-
const folderId = ui.item.attr('id');
7857-
const folderName = ref.folder_id2name(folderId);
7858-
const folderAttribs = ref.env.subscriptionrows[folderName];
7859-
7860-
let destName;
7861-
if (event.target === mainFolderList) {
7862-
destName = '*';
7863-
} else {
7864-
const destId = event.target.parentElement.id;
7865-
destName = ref.folder_id2name(destId);
7866-
}
7867-
7868-
if (!(
7869-
folderAttribs && !folderAttribs[2]
7870-
&& destName != folderName.replace(ref.last_sub_rx, '')
7871-
&& !destName.startsWith(folderName + ref.env.delimiter)
7872-
)) {
7873-
ui.sender.sortable('cancel');
7874-
}
7875-
7876-
const result = await ref.subscription_move_folder(folderName, destName);
7877-
if (!result) {
7878-
ui.sender.sortable('cancel');
7879-
}
7880-
},
7881-
stop: (event, ui) => {
7882-
$('.hover', $folderLists).removeClass('hover');
7883-
if (ui.item.next().is('.protected')) {
7884-
ui.item.parent().sortable('cancel');
7885-
return false;
7886-
}
7887-
},
7888-
update: (event, ui) => {
7889-
// Save the order if the item was moved only within its list. In case it was moved into a (different)
7890-
// sub-list, the order-saving function gets called from the server's response after the relevant folder
7891-
// rows have been re-rendered, and we can save one HTTP request. We don't skip the other function call
7892-
// because in this moment here we don't know yet if the confirmation dialog about moving the folder will
7893-
// be confirmed or cancelled.
7894-
if (ui.item[0].parentElement === event.target) {
7895-
ref.save_reordered_folder_list();
7896-
}
7897-
},
7898-
});
78997820
};
79007821

79017822
this.save_reordered_folder_list = () => {
7902-
const mainList = ref.subscription_list.container.sortable('toArray');
7903-
const subLists = ref.subscription_list.container.find('.ui-sortable').map((i, elem) => ({
7904-
parentId: elem.parentElement.id,
7905-
elems: $(elem).sortable('toArray'),
7906-
})).toArray();
7907-
// Sort sub-lists after their their parent element, so the sorting for the settings page doesn't get confused
7908-
// (which will hook child-folders onto wrong parents if we don't do this).
7909-
subLists.forEach((subList) => {
7910-
mainList.splice(mainList.indexOf(subList.parentId) + 1, 0, ...subList.elems);
7911-
});
7912-
params = mainList.map((e) => e.replace(/^rcmli/, 'folderorder[]=')).join('&');
7823+
const items = ref.subscription_list.sortable_get_items();
7824+
if (!items) {
7825+
console.error('Failed to get sorted items from folder list, cannot save.');
7826+
return false;
7827+
}
7828+
const params = items.map((e) => e.replace(/^rcmli/, 'folderorder[]=')).join('&');
79137829
this.http_post('folder-reorder', params, this.display_message('', 'loading'));
79147830
};
79157831

@@ -7999,8 +7915,18 @@ function rcube_webmail() {
79997915
}
80007916
};
80017917

8002-
this.subscription_move_folder = function (from, to) {
8003-
if (from && to !== null && from != to && to != from.replace(this.last_sub_rx, '')) {
7918+
this.subscription_move_folder = function (folderId, destId) {
7919+
const from = rcmail.folder_id2name(folderId);
7920+
const fromAttribs = rcmail.env.subscriptionrows[from];
7921+
7922+
let to;
7923+
if (destId === '*') {
7924+
to = '*';
7925+
} else {
7926+
to = rcmail.folder_id2name(destId);
7927+
}
7928+
7929+
if (from && fromAttribs && !fromAttribs[2] && to !== null && !to.startsWith(from + rcmail.env.delimiter) && from != to && to != from.replace(this.last_sub_rx, '')) {
80047930
var path = from.split(this.env.delimiter),
80057931
basename = path.pop(),
80067932
newname = to === '' || to === '*' ? basename : to + this.env.delimiter + basename;

program/js/treelist.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ function rcube_treelist_widget(node, p) {
106106
this.get_single_selection = get_selection;
107107
this.is_search = is_search;
108108
this.reset_search = reset_search;
109+
this.sortable_init = sortable_init;
110+
this.sortable_cancel = sortable_cancel;
111+
this.sortable_get_items = sortable_get_items;
109112

110113
// ///// startup code (constructor)
111114

@@ -1328,6 +1331,127 @@ function rcube_treelist_widget(node, p) {
13281331
function is_draggable() {
13291332
return !!ui_draggable;
13301333
}
1334+
1335+
function sortable_init() {
1336+
const mainList = node;
1337+
me.sortable_lists = $(mainList.parentElement).find('ul');
1338+
const listContainerElem = $(mainList).parents('#layout-list')[0];
1339+
me.sortable_lists.sortable({
1340+
// We can't use `li.mailbox.protected` here because that would disable moving elements out of protected
1341+
// folders. jQuery UI uses `closest()` with this selector, which makes it impossible to keep main list items
1342+
// and sub-list items apart. We disable sorting protected items via a `mousedown` event in the
1343+
// setup code of this rcube_treelist_widget().
1344+
cancel: 'input, div.treetoggle, .custom-control',
1345+
// Use a custom helper element so we can style it.
1346+
// helper: 'original',
1347+
helper: (event, item) => $('<div>').attr('id', 'rcmdraglayer').text(item.children('a').text().trim()),
1348+
appendTo: 'body', // append the helper element to the body so we can make it float outside the lists.
1349+
cursor: 'pointer',
1350+
cursorAt: { top: 0, left: -20 }, // place the helper element to the right of the mouse pointer.
1351+
tolerance: 'pointer', // Position the placeholder according to the mouse pointer, not the helper element.
1352+
items: '> li.mailbox', // handle only the directly descending items, not those of sub-lists (they get they own instance of $.sortable()
1353+
connectWith: `#${mainList.id}, #${mainList.id} ul`,
1354+
placeholder: 'placeholder', // Class name for the placeholder
1355+
start: (event, ui) => {
1356+
// We need the `ui` in the callback and can't pass it as function argument if the callback is
1357+
// called via `body_mouseup()`, so we store a reference.
1358+
me.sortable_ui = ui;
1359+
me.sortable_mouse_move_handler = (event) => {
1360+
const lastElementUnderCursor = document.elementFromPoint(event.pageX, event.pageY);
1361+
// Cancel the sorting if the mouse is dragged out of the list "column".
1362+
if ($.contains(listContainerElem, lastElementUnderCursor) === false) {
1363+
me.sortable_cancel();
1364+
}
1365+
};
1366+
document.addEventListener('mousemove', me.sortable_mouse_move_handler);
1367+
},
1368+
stop: (event, ui) => {
1369+
// Reset some states.
1370+
document.removeEventListener('mousemove', me.sortable_mouse_move_handler);
1371+
me.sortable_lists.find('.hover').removeClass('hover');
1372+
me.sortable_cancelling = false;
1373+
},
1374+
change: (event, ui) => {
1375+
// Prevent sortable folders being sorted in between (technically: before) protected folders.
1376+
// There is no technical reason for this, we just want it from a UX perspective.
1377+
if (ui.placeholder.next().is('.protected')) {
1378+
ui.placeholder.hide();
1379+
} else {
1380+
ui.placeholder.show();
1381+
}
1382+
},
1383+
over: (event, ui) => {
1384+
// Highlight only the list that the dragged element is hovering over.
1385+
me.sortable_lists.find('.hover').removeClass('hover');
1386+
if (event.target !== mainList) {
1387+
$(event.target).closest('li').addClass('hover');
1388+
}
1389+
},
1390+
// We have to make this async so we can wait for the confirmation dialog.
1391+
receive: async (event, ui) => {
1392+
me.sortable_lists.find('.hover').removeClass('hover');
1393+
if (me.sortable_cancelling) {
1394+
// Don't do anything, the sorting is cancelled.
1395+
return;
1396+
}
1397+
1398+
let destId;
1399+
if (event.target === mainList) {
1400+
destId = '*';
1401+
} else {
1402+
destId = event.target.parentElement.id;
1403+
}
1404+
const result = await rcmail.subscription_move_folder(ui.item.attr('id'), destId);
1405+
if (!result) {
1406+
ui.sender.sortable('cancel');
1407+
}
1408+
},
1409+
update: (event, ui) => {
1410+
me.sortable_lists.find('.hover').removeClass('hover');
1411+
if (me.sortable_cancelling) {
1412+
// Don't do anything, the sorting is cancelled.
1413+
return;
1414+
}
1415+
if (ui.item.next().is('.protected')) {
1416+
ui.item.parent().sortable('cancel');
1417+
return false;
1418+
}
1419+
// Save the order if the item was moved only within its list. In case it was moved into a (different)
1420+
// sub-list, the order-saving function gets called from the server's response after the relevant folder
1421+
// rows have been re-rendered, and we can save one HTTP request. We don't skip the other function call
1422+
// because in this moment here we don't know yet if the confirmation dialog about moving the folder will
1423+
// be confirmed or cancelled.
1424+
if (ui.item[0].parentElement === event.target) {
1425+
rcmail.save_reordered_folder_list();
1426+
}
1427+
},
1428+
});
1429+
1430+
return me;
1431+
}
1432+
1433+
function sortable_cancel() {
1434+
me.sortable_lists.find('.hover').removeClass('hover');
1435+
me.sortable_cancelling = true;
1436+
me.sortable_ui.placeholder.hide();
1437+
me.sortable_ui.helper.hide({ duration: 300 });
1438+
me.sortable_ui.item.show({ duration: 300 });
1439+
setTimeout(() => me.sortable_lists.sortable('cancel'), 400);
1440+
}
1441+
1442+
function sortable_get_items() {
1443+
const items = me.container.sortable('toArray');
1444+
const subLists = me.container.find('.ui-sortable').map((i, elem) => ({
1445+
parentId: elem.parentElement.id,
1446+
elems: $(elem).sortable('toArray'),
1447+
})).toArray();
1448+
// Sort sub-lists after their their parent element, so the sorting for the settings page doesn't get confused
1449+
// (which will hook child-folders onto wrong parents if we don't do this).
1450+
subLists.forEach((subList) => {
1451+
items.splice(items.indexOf(subList.parentId) + 1, 0, ...subList.elems);
1452+
});
1453+
return items;
1454+
}
13311455
}
13321456

13331457
// use event processing functions from Roundcube's rcube_event_engine

0 commit comments

Comments
 (0)