feat: saving on toggle
This commit is contained in:
147
src/hooks/useCheckboxCommit.hook.ts
Normal file
147
src/hooks/useCheckboxCommit.hook.ts
Normal file
@@ -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> | string | undefined
|
||||||
|
initialContent: Ref<string> | string
|
||||||
|
initialSha: Ref<string> | 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, defineAsyncComponent, watch } from "vue"
|
import { computed, defineAsyncComponent, nextTick, ref, watch } from "vue"
|
||||||
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
||||||
import { useFile } from "@/hooks/useFile.hook"
|
import { useCheckboxCommit } from "@/hooks/useCheckboxCommit.hook"
|
||||||
import { computedAsync } from "@vueuse/core"
|
import { useMarkdown } from "@/hooks/useMarkdown.hook"
|
||||||
|
import { queryFileContent } from "@/modules/repo/services/repo"
|
||||||
|
import { decodeBase64ToUTF8 } from "@/utils/decodeBase64ToUTF8"
|
||||||
|
|
||||||
const FluxNote = defineAsyncComponent(() => import("@/components/FluxNote.vue"))
|
const FluxNote = defineAsyncComponent(() => import("@/components/FluxNote.vue"))
|
||||||
const props = defineProps<{ user: string; repo: string }>()
|
const props = defineProps<{ user: string; repo: string }>()
|
||||||
@@ -15,14 +17,60 @@ const todoNote = computed(() =>
|
|||||||
store.files.find((file) => file.path?.endsWith("_todo/todo.md")),
|
store.files.find((file) => file.path?.endsWith("_todo/todo.md")),
|
||||||
)
|
)
|
||||||
|
|
||||||
const content = computedAsync(() => {
|
const sha = computed(() => todoNote.value?.sha ?? "")
|
||||||
if (!todoNote.value) {
|
const path = computed(() => todoNote.value?.path)
|
||||||
|
|
||||||
|
const { toHTML } = useMarkdown(repo)
|
||||||
|
|
||||||
|
// Setup checkbox commit handler
|
||||||
|
const {
|
||||||
|
pendingContent,
|
||||||
|
syncContent,
|
||||||
|
listenToCheckboxes,
|
||||||
|
hasPendingChanges,
|
||||||
|
} = useCheckboxCommit({
|
||||||
|
user: props.user,
|
||||||
|
repo: props.repo,
|
||||||
|
path,
|
||||||
|
initialContent: "",
|
||||||
|
initialSha: sha,
|
||||||
|
containerSelector: ".todo-notes .note-display",
|
||||||
|
debounceMs: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Render pending content to HTML for display
|
||||||
|
const renderedContent = computed(() => {
|
||||||
|
if (!pendingContent.value) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
const { getContent } = useFile(todoNote.value?.sha ?? "", false)
|
return toHTML(pendingContent.value)
|
||||||
|
|
||||||
return getContent()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Fetch raw content when sha changes
|
||||||
|
watch(
|
||||||
|
sha,
|
||||||
|
async (newSha) => {
|
||||||
|
if (!newSha || hasPendingChanges.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const base64Content = await queryFileContent(props.user, props.repo, newSha)
|
||||||
|
if (base64Content) {
|
||||||
|
const rawContent = decodeBase64ToUTF8(base64Content)
|
||||||
|
syncContent(rawContent, newSha)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup checkbox listeners when content renders
|
||||||
|
watch(
|
||||||
|
renderedContent,
|
||||||
|
async () => {
|
||||||
|
await nextTick()
|
||||||
|
listenToCheckboxes()
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -31,7 +79,7 @@ const content = computedAsync(() => {
|
|||||||
key="todo-notes"
|
key="todo-notes"
|
||||||
:user="user"
|
:user="user"
|
||||||
:repo="repo"
|
:repo="repo"
|
||||||
:content="content"
|
:content="renderedContent"
|
||||||
:parse-content="false"
|
:parse-content="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user