99 initFilters ,
1010 setTasks ,
1111 getFilteredTasks ,
12+ getOrderedCachedTasks ,
1213 getCurrentFilter ,
1314 getTaskCounts ,
1415 getCompletedThisWeekCount ,
@@ -34,11 +35,21 @@ const sortModeSelect = document.getElementById("sort-mode");
3435const themeToggleBtn = document . getElementById ( "theme-toggle" ) ;
3536const installBtn = document . getElementById ( "install-btn" ) ;
3637const weeklySummaryEl = document . getElementById ( "task-weekly-summary" ) ;
37- const advancedFieldsToggleBtn = document . getElementById ( "toggle-advanced-fields" ) ;
38- const advancedFieldsContainer = document . getElementById ( "advanced-task-options" ) ;
39- const advancedFiltersToggleBtn = document . getElementById ( "toggle-advanced-filters" ) ;
40- const advancedFiltersContainer = document . getElementById ( "advanced-filters-container" ) ;
41- const advancedFiltersToggleContainer = document . querySelector ( ".advanced-filters-toggle-container" ) ;
38+ const advancedFieldsToggleBtn = document . getElementById (
39+ "toggle-advanced-fields" ,
40+ ) ;
41+ const advancedFieldsContainer = document . getElementById (
42+ "advanced-task-options" ,
43+ ) ;
44+ const advancedFiltersToggleBtn = document . getElementById (
45+ "toggle-advanced-filters" ,
46+ ) ;
47+ const advancedFiltersContainer = document . getElementById (
48+ "advanced-filters-container" ,
49+ ) ;
50+ const advancedFiltersToggleContainer = document . querySelector (
51+ ".advanced-filters-toggle-container" ,
52+ ) ;
4253
4354// Global Variables
4455let activeTab = "schedule" ; // or: no-time
@@ -74,9 +85,7 @@ submitFormBtn.addEventListener("click", submitForm);
7485// toggle advanced (optional) fields visibility
7586if ( advancedFieldsToggleBtn && advancedFieldsContainer ) {
7687 // start collapsed
77- advancedFieldsContainer . classList . remove (
78- "advanced-task-options--open" ,
79- ) ;
88+ advancedFieldsContainer . classList . remove ( "advanced-task-options--open" ) ;
8089 advancedFieldsToggleBtn . setAttribute ( "aria-expanded" , "false" ) ;
8190
8291 advancedFieldsToggleBtn . addEventListener ( "click" , ( ) => {
@@ -93,19 +102,18 @@ if (advancedFieldsToggleBtn && advancedFieldsContainer) {
93102 "aria-expanded" ,
94103 ! isOpen ? "true" : "false" ,
95104 ) ;
96- document . querySelector ( ".advanced-fields-toggle-text" ) . textContent = ! isOpen
97- ? "Hide additional options"
98- : "Show additional options" ;
99- document . querySelector ( ".advanced-fields-toggle i" ) . classList . toggle ( "active" , ! isOpen ) ;
105+ document . querySelector ( ".advanced-fields-toggle-text" ) . textContent =
106+ ! isOpen ? "Hide additional options" : "Show additional options" ;
107+ document
108+ . querySelector ( ".advanced-fields-toggle i" )
109+ . classList . toggle ( "active" , ! isOpen ) ;
100110 } ) ;
101111}
102112
103113// toggle advanced filters visibility
104114if ( advancedFiltersToggleBtn && advancedFiltersContainer ) {
105115 // start collapsed
106- advancedFiltersContainer . classList . remove (
107- "tasks-advanced-filters--open" ,
108- ) ;
116+ advancedFiltersContainer . classList . remove ( "tasks-advanced-filters--open" ) ;
109117 advancedFiltersToggleBtn . setAttribute ( "aria-expanded" , "false" ) ;
110118
111119 advancedFiltersToggleBtn . addEventListener ( "click" , ( ) => {
@@ -122,13 +130,44 @@ if (advancedFiltersToggleBtn && advancedFiltersContainer) {
122130 "aria-expanded" ,
123131 ! isOpen ? "true" : "false" ,
124132 ) ;
125- document . querySelector ( ".advanced-filters-toggle-text" ) . textContent = ! isOpen
126- ? "Hide advanced filters"
127- : "Show advanced filters" ;
128- document . querySelector ( "#toggle-advanced-filters i" ) . classList . toggle ( "active" , ! isOpen ) ;
133+ document . querySelector ( ".advanced-filters-toggle-text" ) . textContent =
134+ ! isOpen ? "Hide advanced filters" : "Show advanced filters" ;
135+ document
136+ . querySelector ( "#toggle-advanced-filters i" )
137+ . classList . toggle ( "active" , ! isOpen ) ;
129138 } ) ;
130139}
131140
141+ // Reorder tasks: move task with sourceId before task with targetId, persist order to IndexedDB
142+ async function reorderTasks ( sourceId , targetId ) {
143+ if ( ! sourceId || ! targetId || sourceId === targetId ) return ;
144+ const ordered = getOrderedCachedTasks ( ) ;
145+ const sourceIndex = ordered . findIndex ( ( t ) => t && t . id === sourceId ) ;
146+ const targetIndex = ordered . findIndex ( ( t ) => t && t . id === targetId ) ;
147+ if ( sourceIndex === - 1 || targetIndex === - 1 ) return ;
148+
149+ const [ moved ] = ordered . splice ( sourceIndex , 1 ) ;
150+ ordered . splice ( targetIndex , 0 , moved ) ;
151+ ordered . forEach ( ( task , i ) => {
152+ if ( task ) task . order = i ;
153+ } ) ;
154+
155+ try {
156+ await Promise . all (
157+ ordered . map ( ( task ) => updateTask ( task , task . id ) ) ,
158+ ) ;
159+ setTasks ( ordered ) ;
160+ cachedTasks = ordered ;
161+ const tasksToRender = getFilteredTasks ( ) ;
162+ renderTasksInPage ( tasksToRender ) ;
163+ updateFilterBadges ( ) ;
164+ updateWeeklySummary ( ) ;
165+ showToast ( "✓ Task order updated" , "success" , 2000 ) ;
166+ } catch ( err ) {
167+ showToast ( "✗ Could not save order. Please try again." , "error" , 3000 ) ;
168+ }
169+ }
170+
132171// initialize filters module for filter buttons and advanced filters
133172initFilters (
134173 filterButtons ,
@@ -376,10 +415,7 @@ async function markTaskCompleted(id, iconEl) {
376415
377416 const icon = iconEl . querySelector ( "i" ) ;
378417 if ( icon ) {
379- icon . classList . toggle (
380- "fa-circle-check" ,
381- newStatus === "completed" ,
382- ) ;
418+ icon . classList . toggle ( "fa-circle-check" , newStatus === "completed" ) ;
383419 icon . classList . toggle ( "fa-circle" , newStatus !== "completed" ) ;
384420 icon . classList . toggle ( "text-success" , newStatus === "completed" ) ;
385421 }
@@ -407,13 +443,25 @@ async function markTaskCompleted(id, iconEl) {
407443 }
408444}
409445
446+ // Ensure every task has an order; sort by order (tasks without order go to end)
447+ function normalizeTaskOrder ( tasks ) {
448+ if ( ! Array . isArray ( tasks ) || tasks . length === 0 ) return ;
449+ const sorted = [ ...tasks ] . sort (
450+ ( a , b ) => ( a . order ?? Infinity ) - ( b . order ?? Infinity ) ,
451+ ) ;
452+ sorted . forEach ( ( task , i ) => {
453+ if ( task ) task . order = i ;
454+ } ) ;
455+ }
456+
410457// Update Tasks Container
411458async function updateUI ( ) {
412459 try {
413460 // show loading state while fetching from IndexedDB
414461 showLoadingState ( ) ;
415462
416- const allTasks = await getAllTasksFromDB ( ) ;
463+ let allTasks = await getAllTasksFromDB ( ) ;
464+ normalizeTaskOrder ( allTasks ) ;
417465 cachedTasks = allTasks ;
418466
419467 // Ensure any tasks whose endTime has passed are marked as overdue in DB
@@ -442,10 +490,10 @@ async function updateUI() {
442490 renderTasksInPage ( tasksToRender ) ;
443491 updateFilterBadges ( ) ;
444492 updateWeeklySummary ( ) ;
445-
493+
446494 // show advanced filters toggle button if there are tasks
447495 if ( cachedTasks . length > 0 && advancedFiltersToggleContainer ) {
448- advancedFiltersToggleContainer . classList . remove ( ' d-none' ) ;
496+ advancedFiltersToggleContainer . classList . remove ( " d-none" ) ;
449497 }
450498 } catch ( error ) {
451499 showToast (
@@ -479,7 +527,7 @@ async function submitForm(e) {
479527 // validate form data
480528 if ( ! checkInputValidity ( taskTitle , taskTime , taskDescription ) ) return ;
481529
482- // create task object
530+ // create task object (order = append at end)
483531 const task = {
484532 id : `${ taskTitle } - ${ Date . now ( ) } - ${ Math . random ( ) } ` , // generate unique id for each task
485533 title : taskTitle ,
@@ -488,6 +536,7 @@ async function submitForm(e) {
488536 status : "pending" , // pending, completed, overdue
489537 priority : taskPriority || "Medium" ,
490538 tags,
539+ order : getOrderedCachedTasks ( ) . length ,
491540 } ;
492541
493542 // store form data in IndexedDB
@@ -924,6 +973,26 @@ function showTaskLists(tasks) {
924973 "justify-content-between" ,
925974 ) ;
926975
976+ // Drag handle (only this starts drag to avoid conflicting with buttons)
977+ const dragHandle = document . createElement ( "span" ) ;
978+ dragHandle . classList . add ( "task-drag-handle" , "icon-btn" ) ;
979+ dragHandle . setAttribute ( "draggable" , "true" ) ;
980+ dragHandle . setAttribute ( "aria-label" , "Drag to reorder" ) ;
981+ dragHandle . setAttribute ( "title" , "Drag to reorder" ) ;
982+ const gripIcon = document . createElement ( "i" ) ;
983+ gripIcon . classList . add ( "fa-solid" , "fa-grip-vertical" , "text-muted" ) ;
984+ dragHandle . appendChild ( gripIcon ) ;
985+ dragHandle . addEventListener ( "dragstart" , ( e ) => {
986+ e . stopPropagation ( ) ;
987+ e . dataTransfer . setData ( "text/plain" , task . id ) ;
988+ e . dataTransfer . effectAllowed = "move" ;
989+ e . dataTransfer . setDragImage ( li , 0 , 0 ) ;
990+ li . classList . add ( "task-dragging" ) ;
991+ } ) ;
992+ dragHandle . addEventListener ( "dragend" , ( ) => {
993+ li . classList . remove ( "task-dragging" ) ;
994+ } ) ;
995+
927996 // Content
928997 const content = document . createElement ( "div" ) ;
929998 content . classList . add ( "content" ) ;
@@ -1031,10 +1100,7 @@ function showTaskLists(tasks) {
10311100 deleteBtn . type = "button" ;
10321101 deleteBtn . classList . add ( "icon-btn" , "task-delete-btn" ) ;
10331102 deleteBtn . setAttribute ( "data-id" , task . id ) ;
1034- deleteBtn . setAttribute (
1035- "aria-label" ,
1036- `Delete task "${ task . title } "` ,
1037- ) ;
1103+ deleteBtn . setAttribute ( "aria-label" , `Delete task "${ task . title } "` ) ;
10381104
10391105 const deleteIcon = document . createElement ( "i" ) ;
10401106 deleteIcon . classList . add (
@@ -1070,6 +1136,7 @@ function showTaskLists(tasks) {
10701136 actions . append ( completeBtn ) ;
10711137 }
10721138 actions . append ( deleteBtn , toggleBtn ) ;
1139+ wrapper . prepend ( dragHandle ) ;
10731140 wrapper . append ( content , actions ) ;
10741141
10751142 // Time / status
@@ -1110,6 +1177,26 @@ function showTaskLists(tasks) {
11101177 }
11111178
11121179 li . append ( wrapper , timeSpan ) ;
1180+
1181+ // Drag-and-drop: allow dropping on this item to reorder
1182+ li . addEventListener ( "dragover" , ( e ) => {
1183+ e . preventDefault ( ) ;
1184+ e . dataTransfer . dropEffect = "move" ;
1185+ if ( ! li . classList . contains ( "task-dragging" ) ) {
1186+ li . classList . add ( "task-drag-over" ) ;
1187+ }
1188+ } ) ;
1189+ li . addEventListener ( "dragleave" , ( ) => {
1190+ li . classList . remove ( "task-drag-over" ) ;
1191+ } ) ;
1192+ li . addEventListener ( "drop" , ( e ) => {
1193+ e . preventDefault ( ) ;
1194+ li . classList . remove ( "task-drag-over" ) ;
1195+ const sourceId = e . dataTransfer . getData ( "text/plain" ) ;
1196+ if ( ! sourceId || sourceId === task . id ) return ;
1197+ reorderTasks ( sourceId , task . id ) ;
1198+ } ) ;
1199+
11131200 fragment . appendChild ( li ) ;
11141201 } ) ;
11151202
0 commit comments