feat: use plain text editor for checklist items
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import type { DraggableProvided } from "react-beautiful-dnd";
|
import type { DraggableProvided } from "react-beautiful-dnd";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import ContentEditable from "react-contenteditable";
|
|
||||||
import { HiXMark } from "react-icons/hi2";
|
import { HiXMark } from "react-icons/hi2";
|
||||||
import { RiDraggable } from "react-icons/ri";
|
import { RiDraggable } from "react-icons/ri";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import PlainTextEditor from "~/components/PlainTextEditor";
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { invalidateCard } from "~/utils/cardInvalidation";
|
import { invalidateCard } from "~/utils/cardInvalidation";
|
||||||
@@ -33,9 +33,7 @@ export default function ChecklistItemRow({
|
|||||||
}: ChecklistItemRowProps) {
|
}: ChecklistItemRowProps) {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
|
const [completed, setCompleted] = useState(item.completed);
|
||||||
const [title, setTitle] = useState("");
|
|
||||||
const [completed, setCompleted] = useState(false);
|
|
||||||
|
|
||||||
const updateItem = api.checklist.updateItem.useMutation({
|
const updateItem = api.checklist.updateItem.useMutation({
|
||||||
onMutate: async (vars) => {
|
onMutate: async (vars) => {
|
||||||
@@ -103,21 +101,6 @@ export default function ChecklistItemRow({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only resync from props when switching items to avoid clobbering edits
|
|
||||||
useEffect(() => {
|
|
||||||
setTitle(item.title);
|
|
||||||
setCompleted(item.completed);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [item.publicId]);
|
|
||||||
|
|
||||||
const sanitizeHtmlToPlainText = (html: string): string =>
|
|
||||||
html
|
|
||||||
.replace(/<br\s*\/?>(\n)?/gi, "\n")
|
|
||||||
.replace(/<div><br\s*\/?><\/div>/gi, "")
|
|
||||||
.replace(/<[^>]*>/g, "")
|
|
||||||
.replace(/ /g, " ")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
const handleToggleCompleted = () => {
|
const handleToggleCompleted = () => {
|
||||||
if (viewOnly) return;
|
if (viewOnly) return;
|
||||||
setCompleted((prev) => !prev);
|
setCompleted((prev) => !prev);
|
||||||
@@ -127,14 +110,8 @@ export default function ChecklistItemRow({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const commitTitle = (rawHtml: string) => {
|
const commitTitle = (plain: string) => {
|
||||||
if (viewOnly) return;
|
if (!plain || plain === item.title) return;
|
||||||
const plain = sanitizeHtmlToPlainText(rawHtml);
|
|
||||||
if (!plain || plain === item.title) {
|
|
||||||
setTitle(item.title);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTitle(plain);
|
|
||||||
updateItem.mutate({
|
updateItem.mutate({
|
||||||
checklistItemPublicId: item.publicId,
|
checklistItemPublicId: item.publicId,
|
||||||
title: plain,
|
title: plain,
|
||||||
@@ -183,36 +160,26 @@ export default function ChecklistItemRow({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="flex-1 pr-7">
|
<div className="flex-1 pr-7">
|
||||||
<ContentEditable
|
<PlainTextEditor
|
||||||
html={title}
|
key={item.publicId}
|
||||||
disabled={viewOnly}
|
content={item.title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
readOnly={viewOnly}
|
||||||
// @ts-expect-error - valid event
|
placeholder={t`Add details...`}
|
||||||
onBlur={(e: Event) => {
|
onBlur={commitTitle}
|
||||||
const innerHTML = (e.target as HTMLElement).innerHTML;
|
onEnter={(plain) => {
|
||||||
commitTitle(innerHTML);
|
commitTitle(plain);
|
||||||
|
onCreateNewItem?.();
|
||||||
}}
|
}}
|
||||||
|
onEscape={() => undefined}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"m-0 min-h-[20px] w-full p-0 text-sm leading-[20px] text-light-950 outline-none focus-visible:outline-none dark:text-dark-950",
|
"m-0 min-h-[20px] w-full p-0 text-sm leading-[20px] text-light-950 dark:text-dark-950",
|
||||||
viewOnly && "cursor-default",
|
viewOnly && "cursor-default",
|
||||||
)}
|
)}
|
||||||
placeholder={t`Add details...`}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (viewOnly) return;
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
const innerHTML = (e.currentTarget as HTMLElement).innerHTML;
|
|
||||||
commitTitle(innerHTML);
|
|
||||||
onCreateNewItem?.();
|
|
||||||
}
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
e.preventDefault();
|
|
||||||
setTitle(item.title);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!viewOnly && (
|
{!viewOnly && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user