Skip to content

Commit

Permalink
[NEW] add drag/drop feature to move messages between folders
Browse files Browse the repository at this point in the history
  • Loading branch information
jacob-js authored and josaphatim committed Jan 14, 2024
1 parent 6a4d631 commit 6f825b8
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 4 deletions.
2 changes: 1 addition & 1 deletion modules/core/output_modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ protected function output() {
$js_lib .= '<script type="text/javascript" src="third_party/tingle.min.js"></script>';
$js_lib .= '<script type="text/javascript" src="third_party/ays-beforeunload-shim.js"></script>';
$js_lib .= '<script type="text/javascript" src="third_party/jquery.are-you-sure.js"></script>';

$js_lib .= '<script type="text/javascript" src="third_party/sortable.min.js"></script>';
if ($this->get('encrypt_ajax_requests', '') || $this->get('encrypt_local_storage', '')) {
$js_lib .= '<script type="text/javascript" src="third_party/forge.min.js"></script>';
}
Expand Down
10 changes: 10 additions & 0 deletions modules/core/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,13 @@ div.unseen, .unseen .subject { font-weight: 700; }
.mobile #list_controls_menu.show { display: flex; }
.mobile .imap_sort { width: 100%; }
.snoozed_date { color: teal !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;
}
150 changes: 150 additions & 0 deletions modules/core/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -1861,3 +1861,153 @@ function listControlsMenu() {
$('#list_controls_menu').toggleClass('show')
$('.list_sources').hide();
}


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

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 = hm_page_name();
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(hm_list_path());
}
}
);

// 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);
}
6 changes: 5 additions & 1 deletion modules/themes/assets/blue.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
div { background-color: transparent !important; }
div:not(.dragged_element) { background-color: transparent !important; }
html * { color: #404a5d !important; border-color: #cfdeff !important; }
body, .folder_cell { background: none !important; background-color: transparent !important; }
html { background: linear-gradient(0deg, #e8f0ff, #ecf2ff) !important; }
Expand All @@ -20,3 +20,7 @@ textarea, select, input, .toggle_link { border-color: #ccc !important; color: #6
.sys_messages { background-color: #e8f0ff !important; }
.empty_list { color: #bbb !important; }
.checkbox_cell input[type="checkbox"]:checked + label { background-color: #c6d7ff !important; }
.drop_target { background-color: #cfdeff !important; }
.drag_target {
background-color: #8391b0 !important;
}
4 changes: 2 additions & 2 deletions tests/phpunit/modules/core/modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -1490,10 +1490,10 @@ public function test_page_js_debug() {
$test = new Output_Test('page_js', 'core');
$test->handler_response = array('encrypt_ajax_requests' => true, 'router_module_list' => array('foo', 'core'));
$res = $test->run();
$this->assertEquals(array('<script type="text/javascript" src="third_party/cash.min.js"></script><script type="text/javascript" src="third_party/resumable.min.js"></script><script type="text/javascript" src="third_party/tingle.min.js"></script><script type="text/javascript" src="third_party/ays-beforeunload-shim.js"></script><script type="text/javascript" src="third_party/jquery.are-you-sure.js"></script><script type="text/javascript" src="third_party/forge.min.js"></script><script type="text/javascript" src="modules/core/site.js"></script>'), $res->output_response);
$this->assertEquals(array('<script type="text/javascript" src="third_party/cash.min.js"></script><script type="text/javascript" src="third_party/resumable.min.js"></script><script type="text/javascript" src="third_party/tingle.min.js"></script><script type="text/javascript" src="third_party/ays-beforeunload-shim.js"></script><script type="text/javascript" src="third_party/jquery.are-you-sure.js"></script><script type="text/javascript" src="third_party/sortable.min.js"></script><script type="text/javascript" src="third_party/forge.min.js"></script><script type="text/javascript" src="modules/core/site.js"></script>'), $res->output_response);
$test->handler_response = array('encrypt_ajax_requests' => true, 'router_module_list' => array('imap'));
$res = $test->run();
$this->assertEquals(array('<script type="text/javascript" src="third_party/cash.min.js"></script><script type="text/javascript" src="third_party/resumable.min.js"></script><script type="text/javascript" src="third_party/tingle.min.js"></script><script type="text/javascript" src="third_party/ays-beforeunload-shim.js"></script><script type="text/javascript" src="third_party/jquery.are-you-sure.js"></script><script type="text/javascript" src="third_party/forge.min.js"></script><script type="text/javascript" src="modules/core/site.js"></script><script type="text/javascript" src="modules/imap/site.js"></script>'), $res->output_response);
$this->assertEquals(array('<script type="text/javascript" src="third_party/cash.min.js"></script><script type="text/javascript" src="third_party/resumable.min.js"></script><script type="text/javascript" src="third_party/tingle.min.js"></script><script type="text/javascript" src="third_party/ays-beforeunload-shim.js"></script><script type="text/javascript" src="third_party/jquery.are-you-sure.js"></script><script type="text/javascript" src="third_party/sortable.min.js"></script><script type="text/javascript" src="third_party/forge.min.js"></script><script type="text/javascript" src="modules/core/site.js"></script><script type="text/javascript" src="modules/imap/site.js"></script>'), $res->output_response);
}
/**
* @preserveGlobalState disabled
Expand Down
2 changes: 2 additions & 0 deletions third_party/sortable.min.js

Large diffs are not rendered by default.

0 comments on commit 6f825b8

Please sign in to comment.