mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 11:07:04 +00:00
feat(chapters): focus the title field when adding a chapter
This commit is contained in:
1 parent
efd45cf61e
commit
dad0ac86d0
3 files changed
+40
-7
No files matched your search
@@ -37,6 +37,8 @@ export function EditorView() {
|
||||
const setChapterContent = useBook((s) => s.setChapterContent);
|
||||
const setChapterTitle = useBook((s) => s.setChapterTitle);
|
||||
const setChapterNoTitle = useBook((s) => s.setChapterNoTitle);
|
||||
const pendingTitleFocus = useBook((s) => s.pendingTitleFocus);
|
||||
const clearTitleFocus = useBook((s) => s.clearTitleFocus);
|
||||
const markSaved = useBook((s) => s.markSaved);
|
||||
const theme = useTheme((s) => s.theme);
|
||||
const toggleTheme = useTheme((s) => s.toggle);
|
||||
@@ -88,6 +90,7 @@ export function EditorView() {
|
||||
}, []);
|
||||
|
||||
const focusEditorSoon = useCallback(() => {
|
||||
if (useBook.getState().pendingTitleFocus) return;
|
||||
requestAnimationFrame(() => editorRef.current?.commands.focus(undefined, { scrollIntoView: false }));
|
||||
}, []);
|
||||
|
||||
@@ -420,6 +423,8 @@ export function EditorView() {
|
||||
chapter.noTitle ? "No title" : kind === "body" ? "Chapter title" : kind === "part" ? "Part title (optional)" : "Page title"
|
||||
}
|
||||
disabled={!!chapter.noTitle}
|
||||
focusRequest={pendingTitleFocus === chapter.id}
|
||||
onFocused={clearTitleFocus}
|
||||
onChange={(value) => setChapterTitle(chapter.id, value)}
|
||||
/>
|
||||
{(chapter.noTitle || !chapter.title.trim()) && (
|
||||
@@ -495,11 +500,15 @@ function ChapterTitleInput({
|
||||
value,
|
||||
placeholder,
|
||||
disabled,
|
||||
focusRequest,
|
||||
onFocused,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
placeholder: string;
|
||||
disabled: boolean;
|
||||
focusRequest: boolean;
|
||||
onFocused: () => void;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -511,6 +520,15 @@ function ChapterTitleInput({
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
}, [value, disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusRequest || disabled) return;
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
el.focus();
|
||||
el.select();
|
||||
onFocused();
|
||||
}, [focusRequest, disabled, onFocused]);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
|
||||
@@ -260,7 +260,13 @@ export function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||
)}
|
||||
</ul>
|
||||
{group.kind === "body" && (
|
||||
<button className="add-chapter" onClick={addChapter}>
|
||||
<button
|
||||
className="add-chapter"
|
||||
onClick={() => {
|
||||
addChapter();
|
||||
onNavigate?.();
|
||||
}}
|
||||
>
|
||||
<Icon d="M12 5v14M5 12h14" />
|
||||
New chapter
|
||||
</button>
|
||||
|
||||
+15
-6
@@ -25,6 +25,8 @@ interface BookState {
|
||||
dirty: boolean;
|
||||
exporting: string | null;
|
||||
notice: string | null;
|
||||
pendingTitleFocus: string | null;
|
||||
clearTitleFocus: () => void;
|
||||
setExporting: (label: string | null) => void;
|
||||
setNotice: (message: string | null) => void;
|
||||
openBook: (book: Book) => void;
|
||||
@@ -57,6 +59,8 @@ export const useBook = create<BookState>((set, get) => ({
|
||||
dirty: false,
|
||||
exporting: null,
|
||||
notice: null,
|
||||
pendingTitleFocus: null,
|
||||
clearTitleFocus: () => set({ pendingTitleFocus: null }),
|
||||
setExporting: (label) => set({ exporting: label }),
|
||||
setNotice: (message) => set({ notice: message }),
|
||||
openBook: (book) => {
|
||||
@@ -66,14 +70,14 @@ export const useBook = create<BookState>((set, get) => ({
|
||||
const valid = saved === COVER_ID || normalized.chapters.some((c) => c.id === saved);
|
||||
const activeChapterId = valid ? (saved as string) : normalized.chapters[0]?.id ?? "";
|
||||
applyBookFonts(normalized.settings.fonts);
|
||||
set({ book: normalized, activeChapterId, dirty: false });
|
||||
set({ book: normalized, activeChapterId, dirty: false, pendingTitleFocus: null });
|
||||
},
|
||||
closeBook: () => {
|
||||
flushOutgoing(get().book, get().dirty);
|
||||
resetBookFonts();
|
||||
set({ book: null, activeChapterId: "", dirty: false });
|
||||
set({ book: null, activeChapterId: "", dirty: false, pendingTitleFocus: null });
|
||||
},
|
||||
setActiveChapter: (id) => set({ activeChapterId: id }),
|
||||
setActiveChapter: (id) => set({ activeChapterId: id, pendingTitleFocus: null }),
|
||||
goToAdjacentChapter: (dir) => {
|
||||
const { book, activeChapterId } = get();
|
||||
if (!book) return;
|
||||
@@ -81,7 +85,7 @@ export const useBook = create<BookState>((set, get) => ({
|
||||
const i = ids.indexOf(activeChapterId);
|
||||
if (i === -1) return;
|
||||
const next = ids[(i + dir + ids.length) % ids.length];
|
||||
if (next !== activeChapterId) set({ activeChapterId: next });
|
||||
if (next !== activeChapterId) set({ activeChapterId: next, pendingTitleFocus: null });
|
||||
},
|
||||
setChapterContent: (id, content) =>
|
||||
set((s) =>
|
||||
@@ -110,11 +114,16 @@ export const useBook = create<BookState>((set, get) => ({
|
||||
addChapter: () =>
|
||||
set((s) => {
|
||||
if (!s.book) return {};
|
||||
const chapter = createChapter();
|
||||
const chapter = createChapter("");
|
||||
const chapters = [...s.book.chapters];
|
||||
const backCount = chapters.filter((c) => chapterKind(c) === "back").length;
|
||||
chapters.splice(chapters.length - backCount, 0, chapter);
|
||||
return { activeChapterId: chapter.id, dirty: true, book: { ...s.book, chapters } };
|
||||
return {
|
||||
activeChapterId: chapter.id,
|
||||
pendingTitleFocus: chapter.id,
|
||||
dirty: true,
|
||||
book: { ...s.book, chapters },
|
||||
};
|
||||
}),
|
||||
addPage: (group, title) =>
|
||||
set((s) => {
|
||||
|
||||
Reference in new issue
Block a user