307 lines
7.6 KiB
Svelte
307 lines
7.6 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
addTodoItem,
|
|
addTodoItemBackend,
|
|
getOrCreateTodoList,
|
|
removeTodoItem,
|
|
removeTodoItemBackend,
|
|
serializeTodoList,
|
|
setTodoList,
|
|
toggleTodoItem,
|
|
toggleTodoItemBackend,
|
|
todosStore,
|
|
type TodoItem,
|
|
updateTodoItemText,
|
|
updateTodoItemTextBackend
|
|
} from "$lib/stores/todos";
|
|
import { get } from "svelte/store";
|
|
|
|
export let openDocumentId = "";
|
|
export let openDocumentName = "";
|
|
export let openDocumentContent = "";
|
|
export let onDocumentContentChange: (content: string) => void = () => {};
|
|
|
|
let todoItems: TodoItem[] = [];
|
|
let lastTodoDocumentId = "";
|
|
let newTodoText = "";
|
|
let editingTodoId: number | null = null;
|
|
let editingTodoText = "";
|
|
|
|
async function addTodo() {
|
|
const text = newTodoText.trim();
|
|
if (!text) return;
|
|
newTodoText = "";
|
|
const backendItem = await addTodoItemBackend(openDocumentId, text);
|
|
if (backendItem) {
|
|
todoItems = [backendItem, ...todoItems];
|
|
} else {
|
|
todoItems = addTodoItem(todoItems, text);
|
|
}
|
|
persistTodosForCurrentList();
|
|
}
|
|
|
|
async function toggleTodoDone(id: number) {
|
|
const ok = await toggleTodoItemBackend(openDocumentId, id);
|
|
if (!ok) {
|
|
todoItems = toggleTodoItem(todoItems, id);
|
|
} else {
|
|
todoItems = todoItems.map((t) => (t.id === id ? { ...t, done: !t.done } : t));
|
|
}
|
|
persistTodosForCurrentList();
|
|
}
|
|
|
|
function startEditTodo(id: number) {
|
|
const todo = todoItems.find((item) => item.id === id);
|
|
if (!todo) return;
|
|
editingTodoId = id;
|
|
editingTodoText = todo.text;
|
|
}
|
|
|
|
async function saveEditTodo() {
|
|
if (editingTodoId === null) return;
|
|
const text = editingTodoText.trim();
|
|
if (!text) return;
|
|
const id = editingTodoId;
|
|
editingTodoId = null;
|
|
editingTodoText = "";
|
|
const ok = await updateTodoItemTextBackend(openDocumentId, id, text);
|
|
if (!ok) {
|
|
todoItems = updateTodoItemText(todoItems, id, text);
|
|
} else {
|
|
todoItems = todoItems.map((t) => (t.id === id ? { ...t, text: text.trim() } : t));
|
|
}
|
|
persistTodosForCurrentList();
|
|
}
|
|
|
|
function cancelEditTodo() {
|
|
editingTodoId = null;
|
|
editingTodoText = "";
|
|
}
|
|
|
|
async function removeTodo(id: number) {
|
|
if (editingTodoId === id) {
|
|
cancelEditTodo();
|
|
}
|
|
const ok = await removeTodoItemBackend(openDocumentId, id);
|
|
if (!ok) {
|
|
todoItems = removeTodoItem(todoItems, id);
|
|
} else {
|
|
todoItems = todoItems.filter((t) => t.id !== id);
|
|
}
|
|
persistTodosForCurrentList();
|
|
}
|
|
|
|
function loadTodosForDocument(documentId: string) {
|
|
if (!documentId) {
|
|
todoItems = [];
|
|
return;
|
|
}
|
|
|
|
const lists = get(todosStore);
|
|
const result = getOrCreateTodoList(lists, documentId, openDocumentContent);
|
|
if (result.lists !== lists) {
|
|
todosStore.set(result.lists);
|
|
}
|
|
todoItems = result.todos;
|
|
}
|
|
|
|
function persistTodosForCurrentList() {
|
|
if (!openDocumentId) return;
|
|
const lists = get(todosStore);
|
|
todosStore.set(setTodoList(lists, openDocumentId, todoItems));
|
|
const markdown = serializeTodoList(openDocumentName, todoItems);
|
|
onDocumentContentChange(markdown);
|
|
}
|
|
|
|
$: if (openDocumentId !== lastTodoDocumentId) {
|
|
loadTodosForDocument(openDocumentId);
|
|
editingTodoId = null;
|
|
editingTodoText = "";
|
|
newTodoText = "";
|
|
lastTodoDocumentId = openDocumentId;
|
|
}
|
|
</script>
|
|
|
|
<section class="todo-surface">
|
|
<div class="todo-card">
|
|
<form class="todo-create" on:submit|preventDefault={addTodo}>
|
|
<input
|
|
type="text"
|
|
placeholder="Add a new to-do"
|
|
bind:value={newTodoText}
|
|
aria-label="Add to-do"
|
|
/>
|
|
<button type="submit" class="todo-add-btn">Add</button>
|
|
</form>
|
|
|
|
<ul class="todo-list">
|
|
{#each todoItems as todo}
|
|
<li class="todo-item">
|
|
<label class="todo-check">
|
|
<input type="checkbox" checked={todo.done} on:change={() => toggleTodoDone(todo.id)} />
|
|
</label>
|
|
|
|
{#if editingTodoId === todo.id}
|
|
<input
|
|
type="text"
|
|
class="todo-edit-input"
|
|
bind:value={editingTodoText}
|
|
on:keydown={(event) => {
|
|
if (event.key === "Enter") saveEditTodo();
|
|
if (event.key === "Escape") cancelEditTodo();
|
|
}}
|
|
/>
|
|
<div class="todo-actions">
|
|
<button type="button" class="todo-btn save" on:click={saveEditTodo}>Save</button>
|
|
<button type="button" class="todo-btn ghost" on:click={cancelEditTodo}>Cancel</button>
|
|
</div>
|
|
{:else}
|
|
<span class="todo-text" class:is-done={todo.done}>{todo.text}</span>
|
|
<div class="todo-actions">
|
|
<button type="button" class="todo-btn ghost" on:click={() => startEditTodo(todo.id)}>Edit</button>
|
|
<button type="button" class="todo-btn danger" on:click={() => removeTodo(todo.id)}>Remove</button>
|
|
</div>
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<style>
|
|
.todo-surface {
|
|
min-height: 0;
|
|
flex: 1;
|
|
overflow: auto;
|
|
padding: 0 14px 14px;
|
|
}
|
|
|
|
.todo-card {
|
|
width: min(100%, 920px);
|
|
margin: 0 auto;
|
|
border: none;
|
|
border-radius: 0;
|
|
background: transparent;
|
|
padding: 28px 36px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
max-height: 100%;
|
|
overflow: visible;
|
|
}
|
|
|
|
.todo-create {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.todo-create input,
|
|
.todo-edit-input {
|
|
width: 100%;
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 8px;
|
|
background: color-mix(in srgb, var(--surface-1) 88%, var(--bg-editor) 12%);
|
|
color: var(--text-primary);
|
|
padding: 10px 11px;
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.todo-add-btn {
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border-strong);
|
|
background: color-mix(in srgb, var(--surface-2) 84%, var(--bg-hover) 16%);
|
|
color: var(--text-primary);
|
|
padding: 9px 14px;
|
|
font-size: 0.82rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.todo-add-btn:hover {
|
|
background: var(--bg-hover);
|
|
}
|
|
|
|
.todo-list {
|
|
list-style: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.todo-item {
|
|
display: grid;
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
align-items: center;
|
|
gap: 10px;
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
background: color-mix(in srgb, var(--surface-1) 90%, var(--bg-editor) 10%);
|
|
}
|
|
|
|
.todo-check {
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.todo-text {
|
|
font-size: 0.9rem;
|
|
color: var(--text-primary);
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.todo-text.is-done {
|
|
color: var(--text-dim);
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
.todo-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.todo-btn {
|
|
border-radius: 7px;
|
|
border: 1px solid var(--border-soft);
|
|
background: color-mix(in srgb, var(--surface-1) 90%, var(--bg-editor) 10%);
|
|
color: var(--text-muted);
|
|
padding: 6px 10px;
|
|
font-size: 0.78rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.todo-btn.save {
|
|
border-color: var(--border-strong);
|
|
background: color-mix(in srgb, var(--surface-2) 84%, var(--bg-hover) 16%);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.todo-btn.danger:hover,
|
|
.todo-btn.ghost:hover,
|
|
.todo-btn.save:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
@media (max-width: 980px) {
|
|
.todo-surface {
|
|
padding: 4px 8px 10px;
|
|
}
|
|
|
|
.todo-card {
|
|
width: 100%;
|
|
padding: 18px 16px;
|
|
}
|
|
|
|
.todo-item {
|
|
grid-template-columns: auto minmax(0, 1fr);
|
|
row-gap: 8px;
|
|
}
|
|
|
|
.todo-actions {
|
|
grid-column: 1 / -1;
|
|
justify-content: flex-end;
|
|
}
|
|
}
|
|
</style>
|