diff --git a/src/hooks/useCheckboxCommit.hook.ts b/src/hooks/useCheckboxCommit.hook.ts new file mode 100644 index 0000000..f92ceb5 --- /dev/null +++ b/src/hooks/useCheckboxCommit.hook.ts @@ -0,0 +1,147 @@ +import { ref, Ref, toValue, onUnmounted } from "vue" +import { useDebounceFn } from "@vueuse/core" +import { useGitHubContent } from "@/hooks/useGitHubContent.hook" + +const CHECKBOX_PATTERN = /\[([ xX])\]/g + +const setCheckboxInMarkdown = ( + markdown: string, + index: number, + checked: boolean, +): string => { + let currentIndex = 0 + + return markdown.replace(CHECKBOX_PATTERN, (match) => { + if (currentIndex++ === index) { + return checked ? "[x]" : "[ ]" + } + return match + }) +} + +const findCheckboxIndex = ( + container: Element, + checkbox: HTMLInputElement, +): number => { + const allCheckboxes = container.querySelectorAll('input[type="checkbox"]') + return Array.from(allCheckboxes).indexOf(checkbox) +} + +export const useCheckboxCommit = ({ + user, + repo, + path, + initialContent, + initialSha, + containerSelector, + debounceMs = 1000, +}: { + user: string + repo: string + path: Ref | string | undefined + initialContent: Ref | string + initialSha: Ref | string + containerSelector: string + debounceMs?: number +}) => { + const { updateFile } = useGitHubContent({ user, repo }) + + const pendingContent = ref(toValue(initialContent)) + const currentSha = ref(toValue(initialSha)) + const isCommitting = ref(false) + const hasPendingChanges = ref(false) + + // Update pending content when initial content changes (e.g., after fetch) + const syncContent = (content: string, sha: string) => { + if (!hasPendingChanges.value) { + pendingContent.value = content + currentSha.value = sha + } + } + + const commitChanges = async () => { + const pathValue = toValue(path) + if (!pathValue || !hasPendingChanges.value) { + return + } + + // If already committing, the debounce will re-trigger after + if (isCommitting.value) { + debouncedCommit() + return + } + + isCommitting.value = true + + const newSha = await updateFile({ + content: pendingContent.value, + path: pathValue, + sha: currentSha.value, + }) + + if (newSha) { + currentSha.value = newSha + hasPendingChanges.value = false + } + + isCommitting.value = false + } + + const debouncedCommit = useDebounceFn(commitChanges, debounceMs) + + const handleCheckboxChange = (event: Event) => { + const target = event.target as HTMLInputElement + + if (target.tagName !== "INPUT" || target.type !== "checkbox") { + return + } + + const container = document.querySelector(containerSelector) + if (!container) { + return + } + + const index = findCheckboxIndex(container, target) + if (index === -1) { + return + } + + pendingContent.value = setCheckboxInMarkdown( + pendingContent.value, + index, + target.checked, + ) + hasPendingChanges.value = true + + // Schedule commit + debouncedCommit() + } + + const removeListeners = () => { + const container = document.querySelector(containerSelector) + if (container) { + container.removeEventListener("change", handleCheckboxChange) + } + } + + const listenToCheckboxes = () => { + removeListeners() + const container = document.querySelector(containerSelector) + if (container) { + container.addEventListener("change", handleCheckboxChange) + } + } + + onUnmounted(() => { + removeListeners() + }) + + return { + pendingContent, + currentSha, + isCommitting, + hasPendingChanges, + syncContent, + listenToCheckboxes, + } +} diff --git a/src/views/TodoNotes.vue b/src/views/TodoNotes.vue index b3f055a..5fdaa75 100644 --- a/src/views/TodoNotes.vue +++ b/src/views/TodoNotes.vue @@ -1,8 +1,10 @@