Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f1d3006
Allow to manually reorder "normal" folders
pabzm Jun 24, 2025
68eac0d
Make close_func being called if the dialog is closed, not cancelled
pabzm Jul 25, 2025
3c31ab6
Use Promise to enable waiting for the decision from the dialog
pabzm Jul 25, 2025
2fe4216
WIP: Allow to manually reorder "normal" folders using $.sortable()
pabzm Jul 18, 2025
bacfaaa
Fix showing descendent elements beyond the borders of the li parent
pabzm Jul 25, 2025
89a46a4
Make moving a folder to the bottom less fiddly
pabzm Aug 1, 2025
5fdbb81
Improve colors with hovering in light and dark mode
pabzm Aug 1, 2025
b5bbf44
fixup! 255e51edef58e4881871d06eed70bde4fa122b9d
pabzm Sep 10, 2025
25ce65f
Prevent sortable folders to be sorted between protected folders
pabzm Sep 10, 2025
f09dc47
Allow get_folder_li() for folder sorting
pabzm Oct 22, 2025
e5a4bd3
Move folders up/down with buttons
pabzm Oct 22, 2025
ffea916
A little refactoring (folder_name2id())
pabzm Oct 22, 2025
69e245e
Styling for sortable folders list
pabzm Nov 5, 2025
84af281
Fix code for PHP <8.4
pabzm Nov 5, 2025
590f1e3
Remove another comment
pabzm Nov 5, 2025
4b24592
Add semicolons to conform to the coding style
pabzm Nov 5, 2025
a5cc268
fixup! A little refactoring (folder_name2id())
pabzm Nov 12, 2025
1bf1be7
Stop dragging of sortable items if mouseup happens over an iframe
pabzm Nov 13, 2025
492866d
Save sortable list only if it actually changed
pabzm Nov 13, 2025
0097c19
Fix backgrounds and margins of selected subscriptionlist element in d…
pabzm Nov 13, 2025
db5bdc0
Fix left indention of placeholder when sorting
pabzm Dec 9, 2025
0aa9859
Reduce line height of sortable folders list
pabzm Dec 9, 2025
c61fcf8
Improve code to prevent sorting protected folders
pabzm Dec 9, 2025
5cb808b
Fix left indentation of items that were newly moved into a previously…
pabzm Dec 9, 2025
206c37a
Move sortable folder list code to treelist.js and adapt to previous UX
pabzm Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions program/actions/settings/folder_edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ public static function folder_form($attrib)
'label' => $rcmail->gettext('parentfolder'),
'value' => $select->show($selected),
];

$upBtn = new html_button(['id' => 'move-folder-up', 'class' => 'move-folder-up']);
$downBtn = new html_button(['id' => 'move-folder-down', 'class' => 'move-folder-down']);
$form['props']['fieldsets']['location']['content']['order'] = [
'label' => $rcmail->gettext('reorder_folder'),
'value' => html::div([], [
$upBtn->show($rcmail->gettext('reorder_folder_up')),
$downBtn->show($rcmail->gettext('reorder_folder_down')),
]),
];
}

// Settings
Expand Down
22 changes: 22 additions & 0 deletions program/actions/settings/folder_reorder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

class rcmail_action_settings_folder_reorder extends rcmail_action_settings_folders
{
protected static $mode = self::MODE_AJAX;

/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
#[\Override]
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$list = array_flip(rcube_utils::get_input_value('folderorder', rcube_utils::INPUT_POST, true));
$rcmail->user->save_prefs(['folder_order' => $list]);

$rcmail->output->show_message('successfullysaved', 'confirmation');
$rcmail->output->send();
}
}
5 changes: 5 additions & 0 deletions program/actions/settings/folders.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public function run($args = [])
$storage = $rcmail->get_storage();

$rcmail->output->set_pagetitle($rcmail->gettext('folders'));
$rcmail->output->set_env('folder_ordered_manually', !empty($rcmail->user->get_prefs()['folder_order']));
$rcmail->output->set_env('prefix_ns', $storage->get_namespace('prefix'));
$rcmail->output->set_env('quota', (bool) $storage->get_capability('QUOTA'));
$rcmail->output->include_script('treelist.js');
Expand Down Expand Up @@ -262,6 +263,10 @@ public static function folder_tree_element($folders, &$key, &$js_folders)
'id' => $idx,
'class' => trim($data['class'] . ' mailbox'),
];
// Only allow reordering of non-protected folders.
if ($data['protected']) {
$attribs['class'] .= ' protected';
}

if (!isset($data['level'])) {
$data['level'] = 0;
Expand Down
179 changes: 142 additions & 37 deletions program/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@ function rcube_webmail() {
this.enable_command('save', 'folder-size', true);
parent.rcmail.env.exists = this.env.messagecount;
parent.rcmail.enable_command('purge', this.env.messagecount);
ref.handle_folder_sorting_icons();
} else if (this.env.action == 'responses') {
this.enable_command('add', true);
}
Expand Down Expand Up @@ -771,6 +772,10 @@ function rcube_webmail() {

// catch document (and iframe) mouse clicks
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.subscription_list.sortable_cancel();
}
return ref.doc_mouse_up(e);
};
$(document.body)
Expand Down Expand Up @@ -7793,9 +7798,11 @@ function rcube_webmail() {
id_encode: this.html_identifier_encode,
id_decode: this.html_identifier_decode,
searchbox: '#foldersearch',
sortable: true,
});

this.subscription_list
.sortable_init()
.addEventListener('select', function (node) {
ref.subscription_select(node.id);
})
Expand All @@ -7809,37 +7816,88 @@ function rcube_webmail() {
if (p.query) {
ref.subscription_select();
}
})
.draggable({ cancel: 'li.mailbox.root,input,div.treetoggle,.custom-control' })
.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) {
if (!node.is('.mailbox')) {
return false;
}
});
};

var source_folder = ref.folder_id2name(node.attr('id')),
dest_folder = ref.folder_id2name(this.id),
source = ref.env.subscriptionrows[source_folder],
dest = ref.env.subscriptionrows[dest_folder];
this.save_reordered_folder_list = () => {
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'));
};

return source && !source[2]
&& dest_folder != source_folder.replace(ref.last_sub_rx, '')
&& !dest_folder.startsWith(source_folder + ref.env.delimiter);
},
drop: function (e, ui) {
var source = ref.folder_id2name(ui.draggable.attr('id')),
dest = ref.folder_id2name(this.id);
this.folder_id2name = function (id) {
return id ? ref.html_identifier_decode(id.replace(/^rcmli/, '')) : null;
};

ref.subscription_move_folder(source, dest);
},
this.folder_name2id = function (name) {
if (!name) {
return null;
}
return 'rcmli' + ref.html_identifier_encode(name);
};

this.handle_folder_sorting_icons = function () {
const folder_li = window.parent.rcmail.get_folder_li(ref.env.folder, null, true);
const upIcon = $('#move-folder-up');
const downIcon = $('#move-folder-down');
const prevElem = folder_li.previousElementSibling;
const nextElem = folder_li.nextElementSibling;

upIcon.off('click');
if (prevElem === null || prevElem.classList.contains('protected')) {
upIcon.attr('disabled', 'disabled').addClass('disabled');
} else {
upIcon.attr('disabled', null).removeClass('disabled');
upIcon.on('click', () => {
ref.move_folder_up(ref.env.folder);
ref.handle_folder_sorting_icons();
});
}

downIcon.off('click');
if (nextElem === null || nextElem.classList.contains('protected')) {
downIcon.attr('disabled', 'disabled').addClass('disabled');
} else {
downIcon.attr('disabled', null).removeClass('disabled');
downIcon.on('click', () => {
ref.move_folder_down(ref.env.folder);
ref.handle_folder_sorting_icons();
});
}
};

this.folder_id2name = function (id) {
return id ? ref.html_identifier_decode(id.replace(/^rcmli/, '')) : null;
this.move_folder_up = function (name) {
if (ref.is_framed()) {
return window.parent.rcmail.move_folder_up(name);
}
const elem = ref.get_folder_li(name, null, true);
if (!elem || elem.classList.contains('protected')) {
return;
}
const prevSibling = elem.previousElementSibling;
if (prevSibling && !prevSibling.classList.contains('protected')) {
prevSibling.before(elem);
}
ref.save_reordered_folder_list();
};

this.move_folder_down = function (name) {
if (ref.is_framed()) {
return window.parent.rcmail.move_folder_down(name);
}
const elem = ref.get_folder_li(name, null, true);
if (!elem || elem.classList.contains('protected')) {
return;
}
const nextSibling = elem.nextElementSibling;
if (nextSibling) {
nextSibling.after(elem);
}
ref.save_reordered_folder_list();
};

this.subscription_select = function (id) {
Expand All @@ -7857,19 +7915,46 @@ 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;

if (newname != from) {
this.confirm_dialog(this.get_label('movefolderconfirm'), 'move', function () {
ref.http_post('rename-folder', { _folder_oldname: from, _folder_newname: newname },
ref.set_busy(true, 'foldermoving'));
}, { button_class: 'save move' });
return new Promise((resolve, _reject) => {
this.confirm_dialog(
this.get_label('movefolderconfirm'),
'move',
function () {
ref.http_post('rename-folder',
{
_folder_oldname: from,
_folder_newname: newname,
},
ref.set_busy(true, 'foldermoving')
);
resolve(true);
},
{
button_class: 'save move',
cancel_func: (e, ref) => resolve(false),
}
);
});
}
}
return Promise.resolve(true);
};

// tell server to create and subscribe a new mailbox
Expand All @@ -7891,7 +7976,7 @@ function rcube_webmail() {
};

// Add folder row to the table and initialize it
this.add_folder_row = function (id, name, display_name, is_protected, subscribed, class_name, refrow, subfolders) {
this.add_folder_row = function (id, name, display_name, is_protected, subscribed, class_name, refrow, subfolders, insert_before_elem) {
if (!this.gui_objects.subscriptionlist) {
return false;
}
Expand Down Expand Up @@ -7920,7 +8005,7 @@ function rcube_webmail() {
}

// set ID, reset css class
row.attr({ id: 'rcmli' + this.html_identifier_encode(id), class: class_name });
row.attr({ id: this.folder_name2id(id), class: class_name });

if (!refrow || !refrow.length) {
// remove old data, subfolders and toggle
Expand Down Expand Up @@ -8029,7 +8114,11 @@ function rcube_webmail() {
}
}

if (parent && n == parent) {
if (insert_before_elem && $(insert_before_elem).parents('li')[0] === parent) {
// In this case we theoretically could have skipped the sorting above, but trying to do that resulted in
// strange side effects, so I kept the code in.
$(insert_before_elem).before(row);
} else if (parent && n == parent) {
$('ul', parent).first().append(row);
} else {
while (p = $(n).parent().parent().get(0)) {
Expand Down Expand Up @@ -8070,6 +8159,8 @@ function rcube_webmail() {
this.triggerEvent('clonerow', { row: row, id: id });
}

this.make_folder_lists_sortable();

return row;
};

Expand Down Expand Up @@ -8110,14 +8201,22 @@ function rcube_webmail() {
folder = ref.env.subscriptionrows[fname],
newid = id + fname.slice(prefix_len_id);

this.id = 'rcmli' + ref.html_identifier_encode(newid);
this.id = ref.folder_name2id(newid);
$('input[name="_subscribed[]"]', this).first().val(newid);
folder[0] = name + folder[0].slice(prefix_len_name);

subfolders[newid] = folder;
delete ref.env.subscriptionrows[fname];
});

if (this.env.folder_ordered_manually) {
// We need to store this information now, because it's not available anymore after removing the row from
// the DOM.
next_sibling = row.nextElementSibling;
} else {
next_sibling = null;
}

// get row off the list
row = $(row).detach();

Expand All @@ -8129,7 +8228,11 @@ function rcube_webmail() {
}

// move the existing table row
this.add_folder_row(id, name, display_name, is_protected, subscribed, class_name, row, subfolders);
this.add_folder_row(id, name, display_name, is_protected, subscribed, class_name, row, subfolders, next_sibling);

if (this.env.folder_ordered_manually) {
this.save_reordered_folder_list();
}
};

// remove the table row of a specific mailbox from the table
Expand Down Expand Up @@ -8790,6 +8893,8 @@ function rcube_webmail() {
});
}

options.close = close_func;

return this.show_popup_dialog(content, title, buttons, options);
};

Expand Down Expand Up @@ -8862,7 +8967,7 @@ function rcube_webmail() {
prefix = 'rcmli';
}

if (this.gui_objects.folderlist) {
if (this.gui_objects.folderlist || this.gui_objects.subscriptionlist) {
name = this.html_identifier(name, encode);
return document.getElementById(prefix + name);
}
Expand Down
Loading
Loading