Skip to content

Commit bc15ca5

Browse files
committed
WIP: Allow to manually reorder "normal" folders using $.sortable()
1 parent 1c1349f commit bc15ca5

File tree

3 files changed

+104
-56
lines changed

3 files changed

+104
-56
lines changed

program/js/app.js

Lines changed: 63 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7757,6 +7757,7 @@ function rcube_webmail() {
77577757
id_encode: this.html_identifier_encode,
77587758
id_decode: this.html_identifier_decode,
77597759
searchbox: '#foldersearch',
7760+
sortable: true,
77607761
});
77617762

77627763
this.subscription_list
@@ -7773,59 +7774,74 @@ function rcube_webmail() {
77737774
if (p.query) {
77747775
ref.subscription_select();
77757776
}
7776-
})
7777-
.draggable({ cancel: 'li.mailbox.root,input,div.treetoggle,.custom-control' })
7778-
.droppable({
7779-
// @todo: find better way, accept callback is executed for every folder
7780-
// on the list when dragging starts (and stops), this is slow, but
7781-
// I didn't find a method to check droptarget on over event
7782-
accept: function (node) {
7783-
// Break on .is-being-sorted to enable sorting.
7784-
if (!node.is('.mailbox') || node.is('.is-being-sorted')) {
7785-
return false;
7786-
}
7787-
7788-
var source_folder = ref.folder_id2name(node.attr('id')),
7789-
dest_folder = ref.folder_id2name(this.id),
7790-
source = ref.env.subscriptionrows[source_folder],
7791-
dest = ref.env.subscriptionrows[dest_folder];
7792-
7793-
return source && !source[2]
7794-
&& dest_folder != source_folder.replace(ref.last_sub_rx, '')
7795-
&& !dest_folder.startsWith(source_folder + ref.env.delimiter);
7796-
},
7797-
drop: function (e, ui) {
7798-
var source = ref.folder_id2name(ui.draggable.attr('id')),
7799-
dest = ref.folder_id2name(this.id);
7800-
7801-
ref.subscription_move_folder(source, dest);
7802-
ref.make_folder_lists_sortable();
7803-
},
78047777
});
78057778

78067779
this.make_folder_lists_sortable();
78077780
};
78087781

7782+
// TODO: In the receive callback, can we wait for the confirmation dialog without introducing async/await and Promises?
7783+
// TODO: figure out if the item was moved between protected folders, which is not allowed.
7784+
// TODO: save only if the list is different than at start
7785+
// TODO: Fix the styling (padding)
78097786
this.make_folder_lists_sortable = () => {
7810-
var sortableHandle = $('<div>').addClass('sortable-handle');
7811-
// Destroy and re-create all sortable lists because this gets called in scenarios, in which new lists might
7812-
// exist (e.g. if a folder was moved into another one that previously didn't have child-folders).
7813-
$('ul.ui-sortable').sortable('destroy');
7814-
// (Re-)Add handle icons to every sortable folder.
7815-
$('.sortable-handle').remove();
7816-
$('li.mailbox:not(.protected) > .custom-switch', this.subscription_list.container).prepend(sortableHandle);
7817-
// (Re-)create the sorting.
7818-
$(this.subscription_list.container).parent().find('ul').each((_i, el) => {
7819-
$(el).sortable({
7820-
axis: 'y',
7821-
containment: 'parent',
7822-
items: '> li.mailbox:not(.protected)',
7823-
handle: '.sortable-handle',
7824-
update: () => this.save_reordered_folder_list(),
7825-
// Add/remove a marker to disable dropping on other elements during sorting (see accept() of droppable).
7826-
start: (_ev, ui) => ui.item.addClass('is-being-sorted'),
7827-
stop: (_ev, ui) => ui.item.removeClass('is-being-sorted'),
7828-
});
7787+
const mainFolderList = this.gui_objects.subscriptionlist;
7788+
$folderLists = $('ul', mainFolderList.parentElement);
7789+
$folderLists.sortable({
7790+
axis: 'y',
7791+
// We can't use `li.mailbox.protected` here because that would disable moving elements out of protected
7792+
// folders. jQuery UI uses `closest()` with this selector, which makes it impossible to keep main list items
7793+
// and sub-list items apart. We disable sorting protected items via a `mousedown` event in treelist.js.
7794+
cancel: 'input, div.treetoggle, .custom-control',
7795+
helper: 'clone', // a clone doesn't have the borders, which looks nicer.
7796+
items: '> li.mailbox', // handle only the directly descending items, not those of sub-lists (they get they own instance of $.sortable()
7797+
connectWith: `#${mainFolderList.id}, #${mainFolderList.id} ul`,
7798+
forcePlaceholderSize: true, // Make the placeholder displace the neighboring elements.
7799+
placeholder: 'placeholder', // Class name for the placeholder
7800+
over: (event, ui) => {
7801+
// Highlight the list that the dragged element is hovering over.
7802+
$('.hover', $folderLists).removeClass('hover');
7803+
if (event.target !== mainFolderList) {
7804+
$(event.target).closest('li').addClass('hover');
7805+
}
7806+
},
7807+
receive: async (event, ui) => {
7808+
$('.hover', $folderLists).removeClass('hover');
7809+
const folderId = ui.item.attr('id');
7810+
const folderName = ref.folder_id2name(folderId);
7811+
const folderAttribs = ref.env.subscriptionrows[folderName];
7812+
7813+
let destName;
7814+
if (event.target === mainFolderList) {
7815+
destName = '*';
7816+
} else {
7817+
const destId = event.target.parentElement.id;
7818+
destName = ref.folder_id2name(destId);
7819+
}
7820+
7821+
if (!(
7822+
folderAttribs && !folderAttribs[2]
7823+
&& destName != folderName.replace(ref.last_sub_rx, '')
7824+
&& !destName.startsWith(folderName + ref.env.delimiter)
7825+
)) {
7826+
ui.sender.sortable('cancel');
7827+
}
7828+
7829+
const result = await ref.subscription_move_folder(folderName, destName);
7830+
if (!result) {
7831+
ui.sender.sortable('cancel');
7832+
}
7833+
},
7834+
stop: (event, ui) => {
7835+
$('.hover', $folderLists).removeClass('hover');
7836+
// Save the order if the item was moved only within its list. In case it was moved into a (different)
7837+
// sub-list, the order-saving function gets called from the server's response after the relevant folder
7838+
// rows have been re-rendered, and we can save one HTTP request. We don't skip the other function call
7839+
// because in this moment here we don't know yet if the confirmation dialog about moving the folder will
7840+
// be confirmed or cancelled.
7841+
if (ui.item[0].parentElement === event.target) {
7842+
ref.save_reordered_folder_list();
7843+
}
7844+
},
78297845
});
78307846
};
78317847

program/js/treelist.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ function rcube_treelist_widget(node, p) {
4747
keyboard: true,
4848
tabexit: true,
4949
parent_focus: false,
50+
sortable: false,
5051
check_droptarget: function (node) {
5152
return !node.virtual;
5253
},
@@ -161,6 +162,14 @@ function rcube_treelist_widget(node, p) {
161162
e.stopPropagation();
162163
return false;
163164
}
165+
// Prevent protected folders from being dragged/sorted.
166+
// We can't use the options of $.sortable() for that (then sub-folders of protected folders couldn't be
167+
// dragged, either), thus we implement it here.
168+
if ($(e.target).parent().is('li.mailbox.protected')) {
169+
e.preventDefault();
170+
e.stopPropagation();
171+
return false;
172+
}
164173
});
165174

166175
// activate search function
@@ -804,6 +813,16 @@ function rcube_treelist_widget(node, p) {
804813
}
805814

806815
li.attr('aria-expanded', node.collapsed ? 'false' : 'true');
816+
} else if (p.sortable) {
817+
li.children('div.treetoggle').remove();
818+
// Add an empty sublist if none exists yet, so items can be pulled into it when sorting.
819+
if (li.children('ul').length === 0) {
820+
const ul = $('<ul>').appendTo(li)
821+
.attr('role', 'group');
822+
if (node.childlistclass) {
823+
ul.attr('class', node.childlistclass)
824+
}
825+
}
807826
}
808827
if (li.hasClass('selected')) {
809828
li.attr('aria-selected', 'true');

skins/elastic/styles/widgets/lists.less

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,16 @@ ul.listing {
157157
list-style: none;
158158

159159
ul {
160-
border-top: 1px solid @color-list-border;
160+
min-height: 1.5rem;
161161
padding-left: 1.5em;
162162

163163
li:last-child {
164164
border-bottom: none;
165165
}
166+
167+
li:first-child {
168+
border-top: 1px solid @color-list-border;
169+
}
166170
}
167171

168172
.custom-switch {
@@ -1081,16 +1085,25 @@ html.touch {
10811085
li.mailbox a {
10821086
padding-right: 2.5rem;
10831087
}
1084-
}
10851088

1086-
.sortable-handle {
1087-
display: inline-block;
1089+
ul {
1090+
list-style-type: none;
1091+
display: block;
1092+
min-height: 1rem;
1093+
margin: 0;
1094+
padding: 0 0 0 1rem;
1095+
}
10881096

1089-
&::before {
1090-
font-family: 'Icons';
1091-
font-style: normal;
1092-
.font-icon-solid(@fa-var-arrows-alt-v);
1093-
padding-right: 0.2rem;
1097+
.placeholder {
1098+
border: dashed thin #ccc;
1099+
min-height: 1.5rem;
10941100
}
10951101

1102+
.hover {
1103+
background-color: lightyellow;
1104+
1105+
.placeholder {
1106+
margin-left: 3em;
1107+
}
1108+
}
10961109
}

0 commit comments

Comments
 (0)