Skip to content

Commit

Permalink
Fix moving messages between folders via drag-and-drop (#1233)
Browse files Browse the repository at this point in the history
* [fIX] Move email: Enable drag-and-drop feature to move messages between folders

* Update the dragAndDrop feature

* Refactor messages drag-and-drop handler and ensure the drop target is smoothly highlighted

---------

Co-authored-by: Merci Jacob <[email protected]>
  • Loading branch information
GedeonTS and mercihabam authored Oct 22, 2024
1 parent 5110703 commit ece927d
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 157 deletions.
141 changes: 141 additions & 0 deletions modules/core/js_modules/utils/sortable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
function handleMessagesDragAndDrop() {
const tableBody = document.querySelector('.message_table_body');
if(tableBody && !hm_mobile()) {
const allFoldersClassNames = [];
let targetFolder;
let movingElement;
let movingNumber;
Sortable.create(tableBody, {
sort: false,
group: 'messages',
ghostClass: 'table-secondary',
draggable: 'tr.email',

onMove: (sortableEvent) => {
movingElement = sortableEvent.dragged;
targetFolder = sortableEvent.related?.className.split(' ')[0];
return false;
},

onEnd: () => {
// Remove the highlight class from the tr
document.querySelectorAll('.message_table_body > tr.table-secondary').forEach((row) => {
row.classList.remove('table-secondary');
});
return false;
}
});

const isValidFolderReference = (className='') => {
return className.startsWith('imap_') && allFoldersClassNames.includes(className)
}

alterDragImage(tableBody);

Sortable.utils.on(tableBody, 'dragend', () => {
// If the target is not a folder, do nothing
if (!isValidFolderReference(targetFolder ?? '')) {
return;
}

const page = getPageNameParam();
const selectedRows = [];

if(movingNumber > 1) {
document.querySelectorAll('.message_table_body > tr').forEach(row => {
if (row.querySelector('.checkbox_cell input[type=checkbox]:checked')) {
selectedRows.push(row);
}
});
}

if (selectedRows.length == 0) {
selectedRows.push(movingElement);
}

const movingIds = selectedRows.map(row => row.className.split(' ')[0]);

Hm_Ajax.request(
[{'name': 'hm_ajax_hook', 'value': 'ajax_imap_move_copy_action'},
{'name': 'imap_move_ids', 'value': movingIds.join(',')},
{'name': 'imap_move_to', 'value': targetFolder},
{'name': 'imap_move_page', 'value': page},
{'name': 'imap_move_action', 'value': 'move'}],
(res) =>{
for (const index in res.move_count) {
$('.'+Hm_Utils.clean_selector(res.move_count[index])).remove();
select_imap_folder(getListPathParam());
}
}
);

// Reset the target folder
targetFolder = null;
});


const emailFoldersGroups = document.querySelectorAll('.email_folders .inner_list');
const emailFoldersElements = document.querySelectorAll('.email_folders .inner_list > li');

// Keep track of all folders class names
allFoldersClassNames.push(...[...emailFoldersElements].map(folder => folder.className.split(' ')[0]));

emailFoldersGroups.forEach((emailFolders) => {
Sortable.create(emailFolders, {
sort: false,
group: {
put: 'messages'
}
});
});

emailFoldersElements.forEach((emailFolder) => {
emailFolder.addEventListener('dragover', () => {
emailFolder.classList.add('bg-secondary-subtle');
});
emailFolder.addEventListener('dragleave', () => {
emailFolder.classList.remove('bg-secondary-subtle');
});
emailFolder.addEventListener('drop', () => {
emailFolder.classList.remove('bg-secondary-subtle');
});
});
}
}

function alterDragImage(tableBody) {
Sortable.utils.on(tableBody, 'dragstart', (evt) => {
let movingElements = [];
// Is the target element checked
const isChecked = evt.target.querySelector('.checkbox_cell input[type=checkbox]:checked');
if (isChecked) {
movingElements = document.querySelectorAll('.message_table_body > tr > .checkbox_cell input[type=checkbox]:checked');
// Add a highlight class to the tr
movingElements.forEach((checkbox) => {
checkbox.parentElement.parentElement.classList.add('table-secondary');
});
} else {
// If not, uncheck all other checked elements so that they don't get moved
document.querySelectorAll('.message_table_body > tr > .checkbox_cell input[type=checkbox]:checked').forEach((checkbox) => {
checkbox.checked = false;
});
}

movingNumber = movingElements.length || 1;

const element = document.createElement('div');
element.textContent = `Move ${movingNumber} conversation${movingNumber > 1 ? 's' : ''}`;
element.style.position = 'absolute';
element.className = 'dragged_element';
document.body.appendChild(element);

document.addEventListener('drag', () => {
element.style.display = 'none'
});
document.addEventListener('mouseover', () => {
element.remove();
});

evt.dataTransfer.setDragImage(element, 0, 0);
});
}
6 changes: 0 additions & 6 deletions modules/core/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -1272,17 +1272,11 @@ div.unseen,
color: var(--bs-primary) !important;
}

.drop_target {
background-color: #eee !important;
}
.dragged_element {
background-color: #fff !important;
border: solid 1px #ddd;
padding: 10px;
}
.drag_target {
background-color: #888 !important;
}
.cursor-pointer {
cursor: pointer !important;
}
Expand Down
151 changes: 0 additions & 151 deletions modules/core/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -2088,157 +2088,6 @@ function listControlsMenu() {
$('.list_sources').hide();
}


// Sortablejs
const tableBody = document.querySelector('.message_table_body');
if(tableBody && !hm_mobile()) {
const allFoldersClassNames = [];
let targetFolder;
let movingElement;
let movingNumber;
Sortable.create(tableBody, {
sort: false,
group: 'messages',
ghostClass: 'drag_target',
draggable: ':not(.inline_msg)',

onMove: (sortableEvent) => {
movingElement = sortableEvent.dragged;
targetFolder = sortableEvent.related?.className.split(' ')[0];
return false;
},

onEnd: () => {
// Remove the highlight class from the tr
document.querySelectorAll('.message_table_body > tr.drag_target').forEach((row) => {
row.classList.remove('drag_target');
});
return false;
}
});

const isValidFolderReference = (className='') => {
return className.startsWith('imap_') && allFoldersClassNames.includes(className)
}

Sortable.utils.on(tableBody, 'dragstart', (evt) => {
let movingElements = [];
// Is the target element checked
const isChecked = evt.target.querySelector('.checkbox_cell input[type=checkbox]:checked');
if (isChecked) {
movingElements = document.querySelectorAll('.message_table_body > tr > .checkbox_cell input[type=checkbox]:checked');
// Add a highlight class to the tr
movingElements.forEach((checkbox) => {
checkbox.parentElement.parentElement.classList.add('drag_target');
});
} else {
// If not, uncheck all other checked elements so that they don't get moved
document.querySelectorAll('.message_table_body > tr > .checkbox_cell input[type=checkbox]:checked').forEach((checkbox) => {
checkbox.checked = false;
});
}

movingNumber = movingElements.length || 1;

const element = document.createElement('div');
element.textContent = `Move ${movingNumber} conversation${movingNumber > 1 ? 's' : ''}`;
element.style.position = 'absolute';
element.className = 'dragged_element';
document.body.appendChild(element);

function moveElement() {
element.style.display = 'none';
}

function removeElement() {
element.remove();
}

document.addEventListener('drag', moveElement);
document.addEventListener('mouseover', removeElement);

evt.dataTransfer.setDragImage(element, 0, 0);
});

Sortable.utils.on(tableBody, 'dragend', () => {
// If the target is not a folder, do nothing
if (!isValidFolderReference(targetFolder ?? '')) {
return;
}

const page = getPageNameParam();
const selectedRows = [];

if(movingNumber > 1) {
document.querySelectorAll('.message_table_body > tr').forEach(row => {
if (row.querySelector('.checkbox_cell input[type=checkbox]:checked')) {
selectedRows.push(row);
}
});
}

if (selectedRows.length == 0) {
selectedRows.push(movingElement);
}

const movingIds = selectedRows.map(row => row.className.split(' ')[0]);

Hm_Ajax.request(
[{'name': 'hm_ajax_hook', 'value': 'ajax_imap_move_copy_action'},
{'name': 'imap_move_ids', 'value': movingIds.join(',')},
{'name': 'imap_move_to', 'value': targetFolder},
{'name': 'imap_move_page', 'value': page},
{'name': 'imap_move_action', 'value': 'move'}],
(res) =>{
for (const index in res.move_count) {
$('.'+Hm_Utils.clean_selector(res.move_count[index])).remove();
select_imap_folder(getListPathParam());
}
}
);

// Reset the target folder
targetFolder = null;
});

const folderList = document.querySelector('.folder_list');

const observer = new MutationObserver((mutations) => {
const emailFoldersGroups = document.querySelectorAll('.email_folders .inner_list');
const emailFoldersElements = document.querySelectorAll('.email_folders .inner_list > li');

// Keep track of all folders class names
allFoldersClassNames.push(...[...emailFoldersElements].map(folder => folder.className.split(' ')[0]));

emailFoldersGroups.forEach((emailFolders) => {
Sortable.create(emailFolders, {
sort: false,
group: {
put: 'messages'
}
});
});

emailFoldersElements.forEach((emailFolder) => {
emailFolder.addEventListener('dragenter', () => {
emailFolder.classList.add('drop_target');
});
emailFolder.addEventListener('dragleave', () => {
emailFolder.classList.remove('drop_target');
});
emailFolder.addEventListener('drop', () => {
emailFolder.classList.remove('drop_target');
});
});
});

const config = {
childList: true
};

observer.observe(folderList, config);
}

var resetStepperButtons = function() {
$('.step_config-actions button').removeAttr('disabled');
$('#stepper-action-finish').text($('#stepper-action-finish').text().slice(0, -3));
Expand Down
1 change: 1 addition & 0 deletions modules/imap/js_modules/route_handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ function applyImapMessageListPageHandlers(routeParams) {

imap_setup_snooze();
imap_setup_tags();
handleMessagesDragAndDrop();

Hm_Message_List.set_row_events();

Expand Down

0 comments on commit ece927d

Please sign in to comment.