Initial commit
This commit is contained in:
+782
@@ -0,0 +1,782 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Todo List</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #f5f7fa;
|
||||
--primary-color: #0066ff;
|
||||
--primary-dark: #0052cc;
|
||||
--text-color: #333;
|
||||
--border-color: #ddd;
|
||||
--radius: 8px;
|
||||
--shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.dark-mode {
|
||||
--bg-color: #1e1e1e;
|
||||
--primary-color: #bb86fc;
|
||||
--primary-dark: #a074f2;
|
||||
--text-color: #e0e0e0;
|
||||
--border-color: #444;
|
||||
--shadow: none;
|
||||
}
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
background: var(--bg-color);
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.container {
|
||||
position: relative; /* needed for absolute toggle */
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
.dark-mode .container {
|
||||
background: #2b2b2b;
|
||||
}
|
||||
h1 {
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
#newTodoInput {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius);
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
#newTodoInput:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
#addBtn {
|
||||
background: var(--primary-color);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#addBtn:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
border-radius: var(--radius);
|
||||
background: #fafafa;
|
||||
transition: background 0.2s;
|
||||
gap: 0.5rem;
|
||||
position: relative; /* for highlight overlay */
|
||||
}
|
||||
.dark-mode li {
|
||||
background: #3a3a3a;
|
||||
color: var(--text-color);
|
||||
}
|
||||
li:hover {
|
||||
background: #f0f8ff;
|
||||
}
|
||||
.dark-mode li:hover {
|
||||
background: #4a4a4a;
|
||||
}
|
||||
li.completed .todo-text {
|
||||
text-decoration: line-through;
|
||||
color: #888;
|
||||
}
|
||||
.drag-handle {
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
text-align: center;
|
||||
margin-right: 0.5rem;
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
.drag-handle:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.todo-text {
|
||||
flex: 1;
|
||||
margin-left: 0.75rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
.editing-input {
|
||||
flex: 1;
|
||||
font-size: 1rem;
|
||||
padding: 0.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button.action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #007bff;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button.action-btn:hover {
|
||||
background: #e0e0ff;
|
||||
}
|
||||
button.delete-btn {
|
||||
background: #ff6b6b;
|
||||
color: #fff;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
border: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button.delete-btn:hover {
|
||||
background: #e55a5a;
|
||||
}
|
||||
.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
/* Visual cue – draws a line either ABOVE or BELOW the target li */
|
||||
.indicator-above {
|
||||
border-top: 3px solid var(--primary-color);
|
||||
} /* line ABOVE the item */
|
||||
.indicator-below {
|
||||
border-bottom: 3px solid var(--primary-color);
|
||||
} /* line BELOW the item */
|
||||
.theme-toggle {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
line-height: 1;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
.theme-toggle:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div style="text-align: center; margin-bottom: 1.5rem">
|
||||
<h1
|
||||
id="todoTitle"
|
||||
style="cursor: pointer; display: inline-block"
|
||||
>
|
||||
Todo List
|
||||
</h1>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="newTodoInput"
|
||||
placeholder="What needs to be done?"
|
||||
/>
|
||||
<button id="addBtn">Add</button>
|
||||
</div>
|
||||
<ul id="todoList"></ul>
|
||||
<button
|
||||
id="themeToggle"
|
||||
class="theme-toggle"
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
id="theme-icon"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const input = document.getElementById("newTodoInput");
|
||||
const addBtn = document.getElementById("addBtn");
|
||||
const list = document.getElementById("todoList");
|
||||
const themeToggle = document.getElementById("themeToggle");
|
||||
const todoTitle = document.getElementById("todoTitle");
|
||||
|
||||
// Load theme preference from localStorage
|
||||
if (localStorage.getItem("darkMode") === "enabled") {
|
||||
document.body.classList.add("dark-mode");
|
||||
// Update icon to sun for dark mode
|
||||
themeToggle.innerHTML = `
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="5" fill="currentColor" />
|
||||
<g fill="currentColor">
|
||||
<circle cx="12" cy="2" r="1" />
|
||||
<circle cx="12" cy="22" r="1" />
|
||||
<circle cx="2" cy="12" r="1" />
|
||||
<circle cx="22" cy="12" r="1" />
|
||||
<circle cx="4.22" cy="4.22" r="1" />
|
||||
<circle cx="19.78" cy="19.78" r="1" />
|
||||
<circle cx="4.22" cy="19.78" r="1" />
|
||||
<circle cx="19.78" cy="4.22" r="1" />
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
// Load saved title from localStorage or use default
|
||||
const savedTitle = localStorage.getItem("todoListTitle");
|
||||
if (savedTitle) {
|
||||
todoTitle.textContent = savedTitle;
|
||||
document.title = savedTitle;
|
||||
}
|
||||
|
||||
// Make title editable by clicking
|
||||
todoTitle.addEventListener("click", function () {
|
||||
const currentTitle = todoTitle.textContent;
|
||||
todoTitle.innerHTML = `<input type="text" id="titleInput" value="${currentTitle}" style="font-size: 1.5rem; font-weight: 600; width: 200px; border: 1px solid var(--border-color); padding: 0.25rem; border-radius: var(--radius); text-align: center;">`;
|
||||
const titleInput = document.getElementById("titleInput");
|
||||
titleInput.focus();
|
||||
titleInput.select();
|
||||
|
||||
// Handle saving when pressing Enter or losing focus
|
||||
titleInput.addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
saveTitle();
|
||||
}
|
||||
});
|
||||
|
||||
titleInput.addEventListener("blur", saveTitle);
|
||||
});
|
||||
|
||||
function saveTitle() {
|
||||
const titleInput = document.getElementById("titleInput");
|
||||
if (titleInput) {
|
||||
const newTitle = titleInput.value.trim();
|
||||
if (newTitle) {
|
||||
todoTitle.textContent = newTitle;
|
||||
document.title = newTitle; // Update browser tab title
|
||||
localStorage.setItem("todoListTitle", newTitle); // Save to localStorage
|
||||
} else {
|
||||
todoTitle.textContent = "Todo List"; // Reset to default
|
||||
document.title = "Todo List";
|
||||
localStorage.removeItem("todoListTitle"); // Remove from localStorage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load all todos from localStorage
|
||||
function loadTodos() {
|
||||
const todos = JSON.parse(localStorage.getItem("todos") || "[]");
|
||||
list.innerHTML = "";
|
||||
todos.forEach((todo) => {
|
||||
const li = document.createElement("li");
|
||||
li.dataset.id = todo.id;
|
||||
li.setAttribute("draggable", "true");
|
||||
li.className = todo.completed ? "completed" : "";
|
||||
|
||||
const dragHandle = document.createElement("span");
|
||||
dragHandle.className = "drag-handle";
|
||||
dragHandle.textContent = "≡";
|
||||
dragHandle.title = "Reorder";
|
||||
li.prepend(dragHandle);
|
||||
|
||||
// Create checkbox for strikethrough
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.style.marginRight = "0.5rem";
|
||||
checkbox.style.cursor = "pointer";
|
||||
|
||||
// Set checkbox state based on todo completion
|
||||
if (todo.completed) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
|
||||
// Add event listener to checkbox
|
||||
checkbox.addEventListener("change", function (e) {
|
||||
e.stopPropagation(); // Prevent event from bubbling up to parent
|
||||
|
||||
// Find the todo text span
|
||||
const textSpan = li.querySelector(".todo-text");
|
||||
if (this.checked) {
|
||||
textSpan.style.textDecoration = "line-through";
|
||||
textSpan.style.color = "#888";
|
||||
} else {
|
||||
textSpan.style.textDecoration = "none";
|
||||
textSpan.style.color = "";
|
||||
}
|
||||
|
||||
// Update todo in localStorage
|
||||
const todos = JSON.parse(
|
||||
localStorage.getItem("todos") || "[]",
|
||||
);
|
||||
const updatedTodos = todos.map((t) =>
|
||||
t.id === todo.id
|
||||
? { ...t, completed: this.checked }
|
||||
: t,
|
||||
);
|
||||
|
||||
// Move checked item to correct position (place at bottom of list)
|
||||
if (this.checked) {
|
||||
// Save updated todos to localStorage
|
||||
localStorage.setItem(
|
||||
"todos",
|
||||
JSON.stringify(updatedTodos),
|
||||
);
|
||||
// Mark this li as completed
|
||||
li.classList.add("completed");
|
||||
// Find the last unchecked item to place completed item after it
|
||||
let lastUncheckedItem = null;
|
||||
for (
|
||||
let i = list.children.length - 1;
|
||||
i >= 0;
|
||||
i--
|
||||
) {
|
||||
if (
|
||||
!list.children[i].classList.contains(
|
||||
"completed",
|
||||
)
|
||||
) {
|
||||
lastUncheckedItem = list.children[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are unchecked items, insert after the last unchecked item
|
||||
// Otherwise, append to the end of the list
|
||||
if (lastUncheckedItem) {
|
||||
list.insertBefore(
|
||||
li,
|
||||
lastUncheckedItem.nextSibling,
|
||||
);
|
||||
} else {
|
||||
list.appendChild(li);
|
||||
}
|
||||
|
||||
// Get the new order from DOM and reorder todos
|
||||
const newOrder = Array.from(list.children).map(
|
||||
(li) => parseInt(li.dataset.id),
|
||||
);
|
||||
const reorderedTodos = newOrder.map((id) =>
|
||||
updatedTodos.find((t) => t.id === id),
|
||||
);
|
||||
localStorage.setItem(
|
||||
"todos",
|
||||
JSON.stringify(reorderedTodos),
|
||||
);
|
||||
} else {
|
||||
// Capture the current position of li BEFORE any changes
|
||||
const currentIndex = Array.from(
|
||||
list.children,
|
||||
).indexOf(li);
|
||||
|
||||
// Find the first checked item BEFORE removing completed class
|
||||
const firstCheckedItem = Array.from(
|
||||
list.children,
|
||||
).find((sibling) =>
|
||||
sibling.classList.contains("completed"),
|
||||
);
|
||||
|
||||
// Find the last unchecked item (excluding this li) to place unchecked item after it
|
||||
let lastUncheckedItem = null;
|
||||
for (
|
||||
let i = list.children.length - 1;
|
||||
i >= 0;
|
||||
i--
|
||||
) {
|
||||
const sibling = list.children[i];
|
||||
if (
|
||||
sibling !== li &&
|
||||
!sibling.classList.contains("completed")
|
||||
) {
|
||||
lastUncheckedItem = sibling;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove completed class when unchecking
|
||||
li.classList.remove("completed");
|
||||
|
||||
// If there are checked items, insert before the first checked item
|
||||
// If there are unchecked items before this one, insert after the last unchecked item
|
||||
// Otherwise, append to the end of unchecked items
|
||||
if (firstCheckedItem) {
|
||||
const firstCheckedIndex = Array.from(
|
||||
list.children,
|
||||
).indexOf(firstCheckedItem);
|
||||
const lastIndex = Array.from(
|
||||
list.children,
|
||||
).indexOf(li);
|
||||
|
||||
if (
|
||||
lastUncheckedItem &&
|
||||
lastIndex > 0 &&
|
||||
Array.from(list.children).indexOf(
|
||||
lastUncheckedItem,
|
||||
) < lastIndex
|
||||
) {
|
||||
list.insertBefore(
|
||||
li,
|
||||
lastUncheckedItem.nextSibling,
|
||||
);
|
||||
} else {
|
||||
list.insertBefore(li, firstCheckedItem);
|
||||
}
|
||||
} else if (
|
||||
lastUncheckedItem &&
|
||||
currentIndex > 0 &&
|
||||
Array.from(list.children).indexOf(
|
||||
lastUncheckedItem,
|
||||
) < currentIndex
|
||||
) {
|
||||
list.insertBefore(
|
||||
li,
|
||||
lastUncheckedItem.nextSibling,
|
||||
);
|
||||
}
|
||||
|
||||
// Get the new order from DOM after reordering and save to localStorage
|
||||
const newOrder = Array.from(list.children).map(
|
||||
(li) => parseInt(li.dataset.id),
|
||||
);
|
||||
const reorderedTodos = newOrder.map((id) =>
|
||||
updatedTodos.find((t) => t.id === id),
|
||||
);
|
||||
localStorage.setItem(
|
||||
"todos",
|
||||
JSON.stringify(reorderedTodos),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Insert checkbox at the beginning of the li
|
||||
li.insertBefore(checkbox, li.firstChild);
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.className = "todo-text";
|
||||
span.textContent = todo.text;
|
||||
span.style.cursor = "pointer"; // Make it clear it's editable
|
||||
li.appendChild(span);
|
||||
|
||||
// Make the entire li clickable for editing
|
||||
li.addEventListener("click", (e) => {
|
||||
// Don't trigger edit if clicking on the delete button, edit button, or checkbox
|
||||
if (
|
||||
e.target.classList.contains("delete-btn") ||
|
||||
e.target.classList.contains("action-btn") ||
|
||||
e.target.type === "checkbox"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
startEdit(li);
|
||||
});
|
||||
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.className = "delete-btn";
|
||||
delBtn.textContent = "Delete";
|
||||
delBtn.onclick = () => deleteTodo(todo.id);
|
||||
|
||||
li.appendChild(delBtn);
|
||||
|
||||
// ---------- Drag & Drop Handlers ----------
|
||||
li.addEventListener("dragstart", (e) => {
|
||||
e.dataTransfer.setData("text/plain", todo.id);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
li.classList.add("dragging");
|
||||
});
|
||||
|
||||
li.addEventListener("dragover", (e) => {
|
||||
e.preventDefault(); // allow drop
|
||||
// Remove any existing indicator classes from ALL list items
|
||||
document
|
||||
.querySelectorAll(
|
||||
".indicator-above, .indicator-below",
|
||||
)
|
||||
.forEach((el) =>
|
||||
el.classList.remove(
|
||||
"indicator-above",
|
||||
"indicator-below",
|
||||
),
|
||||
);
|
||||
|
||||
const targetLi = li; // the li we are hovering over
|
||||
const rect = targetLi.getBoundingClientRect();
|
||||
const offsetY = e.clientY - rect.top;
|
||||
const halfway = rect.height / 2;
|
||||
// If cursor is in the upper half of the item, we will INSERT ABOVE it
|
||||
const insertAbove = offsetY < halfway;
|
||||
|
||||
// Store the decision on the element so we can reuse it later
|
||||
targetLi.dataset.insertAbove = insertAbove;
|
||||
|
||||
// Add the appropriate visual indicator class to THIS li only
|
||||
if (insertAbove) {
|
||||
targetLi.classList.add("indicator-above"); // draws a line ABOVE the item
|
||||
} else {
|
||||
targetLi.classList.add("indicator-below"); // draws a line BELOW the item
|
||||
}
|
||||
});
|
||||
|
||||
li.addEventListener("dragleave", (e) => {
|
||||
// Clean up highlight as soon as the cursor leaves this li
|
||||
highlightDropTarget(li, false);
|
||||
});
|
||||
|
||||
li.addEventListener("drop", handleDrop);
|
||||
li.addEventListener("dragend", () => {
|
||||
li.classList.remove("dragging");
|
||||
// Always strip any lingering indicator classes
|
||||
document
|
||||
.querySelectorAll(
|
||||
".indicator-above, .indicator-below",
|
||||
)
|
||||
.forEach((el) =>
|
||||
el.classList.remove(
|
||||
"indicator-above",
|
||||
"indicator-below",
|
||||
),
|
||||
);
|
||||
// Clean the stored flag
|
||||
delete targetLi.dataset.insertAbove;
|
||||
});
|
||||
// --------------------------- ----------------
|
||||
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// Add a new todo
|
||||
async function addTodo() {
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
// Get existing todos from localStorage
|
||||
const todos = JSON.parse(localStorage.getItem("todos") || "[]");
|
||||
|
||||
// Create new todo with unique ID
|
||||
const newTodo = {
|
||||
id: Date.now(),
|
||||
text: text,
|
||||
completed: false,
|
||||
};
|
||||
|
||||
// Add new todo to the array
|
||||
todos.push(newTodo);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem("todos", JSON.stringify(todos));
|
||||
|
||||
input.value = "";
|
||||
loadTodos();
|
||||
}
|
||||
|
||||
// Delete a todo
|
||||
async function deleteTodo(id) {
|
||||
if (!confirm("Delete this todo?")) return;
|
||||
|
||||
// Get existing todos from localStorage
|
||||
let todos = JSON.parse(localStorage.getItem("todos") || "[]");
|
||||
|
||||
// Filter out the todo with the given ID
|
||||
todos = todos.filter((t) => t.id !== id);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem("todos", JSON.stringify(todos));
|
||||
|
||||
loadTodos();
|
||||
}
|
||||
|
||||
// Inline edit
|
||||
function startEdit(li) {
|
||||
const id = li.dataset.id;
|
||||
const textSpan = li.querySelector(".todo-text");
|
||||
if (!textSpan) return;
|
||||
|
||||
const editingInput = document.createElement("input");
|
||||
editingInput.type = "text";
|
||||
editingInput.value = textSpan.textContent;
|
||||
editingInput.className = "editing-input";
|
||||
editingInput.style.width = "100%";
|
||||
editingInput.setAttribute("autofocus", "");
|
||||
|
||||
li.replaceChild(editingInput, textSpan);
|
||||
editingInput.focus();
|
||||
|
||||
function commit() {
|
||||
const newText = editingInput.value.trim();
|
||||
if (newText !== textSpan.textContent) {
|
||||
// Get existing todos from localStorage
|
||||
const todos = JSON.parse(
|
||||
localStorage.getItem("todos") || "[]",
|
||||
);
|
||||
|
||||
// Update todo text
|
||||
const updatedTodos = todos.map((t) =>
|
||||
t.id === id ? { ...t, text: newText } : t,
|
||||
);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem(
|
||||
"todos",
|
||||
JSON.stringify(updatedTodos),
|
||||
);
|
||||
|
||||
loadTodos();
|
||||
} else {
|
||||
loadTodos();
|
||||
}
|
||||
}
|
||||
editingInput.addEventListener("blur", commit);
|
||||
editingInput.addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") commit();
|
||||
});
|
||||
}
|
||||
|
||||
// Highlight the target li with either .indicator-above or .indicator-below
|
||||
function highlightDropTarget(li, remove) {
|
||||
if (!remove) {
|
||||
// Caller will handle removal – we just return here for clarity
|
||||
return;
|
||||
}
|
||||
li.classList.remove("indicator-above", "indicator-below");
|
||||
}
|
||||
|
||||
// Handle drop to reorder items
|
||||
async function handleDrop(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const draggedId = event.dataTransfer.getData("text/plain");
|
||||
const targetLi = event.currentTarget.closest("li");
|
||||
if (!targetLi || targetLi.dataset.id === draggedId) return;
|
||||
|
||||
// Retrieve the insertion side we stored earlier
|
||||
const insertAbove = targetLi.dataset.insertAbove === "true";
|
||||
|
||||
// Remove dragged element from the DOM
|
||||
const draggedLi = list.querySelector(
|
||||
`[data-id="${draggedId}"]`,
|
||||
);
|
||||
if (!draggedLi) return;
|
||||
list.removeChild(draggedLi);
|
||||
|
||||
// Insert based on the stored decision
|
||||
if (insertAbove) {
|
||||
// Insert *before* the target (so dragged item appears ABOVE it)
|
||||
list.insertBefore(draggedLi, targetLi);
|
||||
} else {
|
||||
// Insert *after* the target (so dragged item appears BELOW it)
|
||||
// If target is the last child, just append
|
||||
if (targetLi.nextSibling) {
|
||||
list.insertBefore(draggedLi, targetLi.nextSibling);
|
||||
} else {
|
||||
list.appendChild(draggedLi);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up any lingering indicator classes and the stored flag
|
||||
document
|
||||
.querySelectorAll(".indicator-above, .indicator-below")
|
||||
.forEach((el) =>
|
||||
el.classList.remove(
|
||||
"indicator-above",
|
||||
"indicator-below",
|
||||
),
|
||||
);
|
||||
delete targetLi.dataset.insertAbove;
|
||||
|
||||
// Persist the new order to localStorage
|
||||
const todos = JSON.parse(localStorage.getItem("todos") || "[]");
|
||||
const newOrder = Array.from(list.children).map((li) =>
|
||||
parseInt(li.dataset.id),
|
||||
);
|
||||
const reorderedTodos = newOrder.map((id) =>
|
||||
todos.find((t) => t.id === id),
|
||||
);
|
||||
localStorage.setItem("todos", JSON.stringify(reorderedTodos));
|
||||
}
|
||||
|
||||
// Theme toggle
|
||||
themeToggle.addEventListener("click", () => {
|
||||
document.body.classList.toggle("dark-mode");
|
||||
if (document.body.classList.contains("dark-mode")) {
|
||||
localStorage.setItem("darkMode", "enabled");
|
||||
// Update icon to sun for dark mode
|
||||
themeToggle.innerHTML = `
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="5" fill="currentColor" />
|
||||
<g fill="currentColor">
|
||||
<circle cx="12" cy="2" r="1" />
|
||||
<circle cx="12" cy="22" r="1" />
|
||||
<circle cx="2" cy="12" r="1" />
|
||||
<circle cx="22" cy="12" r="1" />
|
||||
<circle cx="4.22" cy="4.22" r="1" />
|
||||
<circle cx="19.78" cy="19.78" r="1" />
|
||||
<circle cx="4.22" cy="19.78" r="1" />
|
||||
<circle cx="19.78" cy="4.22" r="1" />
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
} else {
|
||||
localStorage.setItem("darkMode", "disabled");
|
||||
// Update icon to moon for light mode
|
||||
themeToggle.innerHTML = `
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="theme-icon" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
// Kick off
|
||||
addBtn.onclick = addTodo;
|
||||
input.addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") addTodo();
|
||||
});
|
||||
loadTodos();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user