Add chapter navigation to the Full Book view
Add a chapter dropdown plus Previous/Next buttons to the Full Book toolbar. The dropdown lists every chapter in canonical order and the buttons jump between them, scrolling the assembled manuscript so the target chapter begins near the top of the viewport. Manual scrolling keeps the dropdown in sync with the chapter currently on screen, and reordering chapters while Full Book is open re-renders navigation to match the new order immediately. Navigation reuses the same chapter boundaries that drive page breaks, so it can never drift from the assembled chapter order.
This commit is contained in:
parent
d510a48742
commit
7011d0655b
6 changed files with 362 additions and 5 deletions
|
|
@ -70,7 +70,8 @@ function createWindow() {
|
|||
const scenario = fs
|
||||
.readFileSync(scenarioPath, "utf-8")
|
||||
.replace(/__BOOKDIR_VALUE__/, JSON.stringify(bookDir))
|
||||
.replace(/__BOOKDIR2_VALUE__/, JSON.stringify(process.env.FOLIO_TEST_BOOK2 || ""));
|
||||
.replace(/__BOOKDIR2_VALUE__/, JSON.stringify(process.env.FOLIO_TEST_BOOK2 || ""))
|
||||
.replace(/__BOOKDIR3_VALUE__/, JSON.stringify(process.env.FOLIO_TEST_BOOK3 || ""));
|
||||
mainWindow?.webContents
|
||||
.executeJavaScript(scenario, false)
|
||||
.then((summary) => {
|
||||
|
|
|
|||
|
|
@ -123,6 +123,9 @@ export function initBook(api: FolioAPI) {
|
|||
const copyAllBtn = $("copyAllBtn") as HTMLButtonElement;
|
||||
const fullBookStatus = $("fullBookStatus");
|
||||
const exportPdfBtn = $("exportPdfBtn") as HTMLButtonElement;
|
||||
const fbPrevBtn = $("fbPrevBtn") as HTMLButtonElement;
|
||||
const fbNextBtn = $("fbNextBtn") as HTMLButtonElement;
|
||||
const fbChapterSelect = $("fbChapterSelect") as HTMLSelectElement;
|
||||
|
||||
// Marks the line where every chapter after the first begins. In print this
|
||||
// line carries the page-break; on screen it shows a subtle separator. The
|
||||
|
|
@ -147,6 +150,94 @@ export function initBook(api: FolioAPI) {
|
|||
extensions: [chapterBreaksField],
|
||||
});
|
||||
|
||||
// Navigation state for the Full Book view. chapterNav tracks every chapter
|
||||
// that produced a block in the assembled manuscript, in canonical order,
|
||||
// along with the offset where its block begins. This is the same boundary
|
||||
// data that drives page breaks, so navigation can never drift from the
|
||||
// assembled order. The current chapter is kept in sync with the scroll
|
||||
// position (manual scrolling updates the selector too).
|
||||
interface FullBookChapterNav {
|
||||
id: string;
|
||||
title: string;
|
||||
start: number;
|
||||
}
|
||||
let fullBookChapters: FullBookChapterNav[] = [];
|
||||
let currentChapterIndex = 0;
|
||||
let navScrollRaf: number | null = null;
|
||||
|
||||
function populateChapterNav(entries: FullBookChapterNav[]) {
|
||||
fullBookChapters = entries;
|
||||
fbChapterSelect.innerHTML = "";
|
||||
for (const e of entries) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = e.id;
|
||||
opt.textContent = e.title;
|
||||
fbChapterSelect.appendChild(opt);
|
||||
}
|
||||
setCurrentChapter(0);
|
||||
}
|
||||
|
||||
function setCurrentChapter(index: number) {
|
||||
if (!fullBookChapters.length) currentChapterIndex = 0;
|
||||
else currentChapterIndex = Math.max(0, Math.min(index, fullBookChapters.length - 1));
|
||||
updateChapterNavUI();
|
||||
}
|
||||
|
||||
function updateChapterNavUI() {
|
||||
const n = fullBookChapters.length;
|
||||
fbPrevBtn.disabled = n === 0 || currentChapterIndex <= 0;
|
||||
fbNextBtn.disabled = n === 0 || currentChapterIndex >= n - 1;
|
||||
const entry = fullBookChapters[currentChapterIndex];
|
||||
if (entry && fbChapterSelect.value !== entry.id) fbChapterSelect.value = entry.id;
|
||||
}
|
||||
|
||||
// Scroll the assembled manuscript so the given chapter's block begins near
|
||||
// the top of the Full Book viewport. This only touches the rendered editor,
|
||||
// never the chapter files or the assembled Markdown.
|
||||
function jumpToChapter(index: number) {
|
||||
if (!fullBookChapters.length) return;
|
||||
const clamped = Math.max(0, Math.min(index, fullBookChapters.length - 1));
|
||||
fullBookEditor.dispatch({
|
||||
effects: EditorView.scrollIntoView(fullBookChapters[clamped].start, { y: "start" }),
|
||||
});
|
||||
setCurrentChapter(clamped);
|
||||
fullBookEditor.focus();
|
||||
}
|
||||
|
||||
// Map the line currently at the top of the Full Book viewport back to the
|
||||
// chapter whose block contains it, so manual scrolling updates the selector.
|
||||
function detectChapterFromScroll(): number {
|
||||
if (!fullBookChapters.length) return 0;
|
||||
const scroller = fullBookEditor.scrollDOM;
|
||||
const rect = scroller.getBoundingClientRect();
|
||||
if (rect.height === 0) return currentChapterIndex;
|
||||
const atBottom = scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 2;
|
||||
const topPos = fullBookEditor.posAtCoords({ x: rect.left + 4, y: rect.top + 4 });
|
||||
if (topPos === null || topPos === undefined) return currentChapterIndex;
|
||||
if (atBottom) {
|
||||
const bottomPos = fullBookEditor.posAtCoords({ x: rect.left + 4, y: rect.bottom - 4 });
|
||||
const last = fullBookChapters[fullBookChapters.length - 1];
|
||||
if (bottomPos !== null && bottomPos !== undefined && bottomPos >= last.start) {
|
||||
return fullBookChapters.length - 1;
|
||||
}
|
||||
}
|
||||
let idx = 0;
|
||||
for (let i = 0; i < fullBookChapters.length; i++) {
|
||||
if (fullBookChapters[i].start <= topPos) idx = i;
|
||||
else break;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
fullBookEditor.scrollDOM.addEventListener("scroll", () => {
|
||||
if (!fullBookChapters.length) return;
|
||||
if (navScrollRaf !== null) cancelAnimationFrame(navScrollRaf);
|
||||
navScrollRaf = requestAnimationFrame(() => {
|
||||
navScrollRaf = null;
|
||||
setCurrentChapter(detectChapterFromScroll());
|
||||
});
|
||||
});
|
||||
|
||||
const editorView: EditorView = createMdEditor(editorHost, {
|
||||
onChange: (text) => handleEditorChange(text),
|
||||
onSelection: (view) => {
|
||||
|
|
@ -389,6 +480,10 @@ export function initBook(api: FolioAPI) {
|
|||
setChapterButtons(false);
|
||||
fullBookBtn.classList.remove("active");
|
||||
setEditorText(fullBookEditor, "");
|
||||
fullBookChapters = [];
|
||||
currentChapterIndex = 0;
|
||||
fbChapterSelect.innerHTML = "";
|
||||
updateChapterNavUI();
|
||||
renderChapterList();
|
||||
}
|
||||
|
||||
|
|
@ -537,15 +632,17 @@ export function initBook(api: FolioAPI) {
|
|||
text: string;
|
||||
chapterStarts: number[];
|
||||
chapters: string[];
|
||||
chapterEntries: { id: string; title: string; start: number }[];
|
||||
targets: WikilinkTarget[];
|
||||
}> {
|
||||
const res = await api.getBookMeta();
|
||||
if (!res || typeof res !== "object" || "error" in res) {
|
||||
return { text: "", chapterStarts: [], chapters: [], targets: [] };
|
||||
return { text: "", chapterStarts: [], chapters: [], chapterEntries: [], targets: [] };
|
||||
}
|
||||
const book = res as BookMeta;
|
||||
const parts: string[] = [];
|
||||
const chapterStarts: number[] = [];
|
||||
const chapterEntries: { id: string; title: string; start: number }[] = [];
|
||||
const targets: WikilinkTarget[] = [];
|
||||
let offset = 0;
|
||||
for (const id of book.chapterOrder) {
|
||||
|
|
@ -566,17 +663,24 @@ export function initBook(api: FolioAPI) {
|
|||
block.push(body);
|
||||
}
|
||||
if (block.length) {
|
||||
chapterEntries.push({ id: ch.id, title: ch.title, start: offset });
|
||||
chapterStarts.push(offset);
|
||||
const joined = block.join("\n");
|
||||
parts.push(joined);
|
||||
offset += joined.length + 2;
|
||||
}
|
||||
}
|
||||
return { text: parts.join("\n\n"), chapterStarts, chapters: parts, targets };
|
||||
return {
|
||||
text: parts.join("\n\n"),
|
||||
chapterStarts,
|
||||
chapters: parts,
|
||||
chapterEntries,
|
||||
targets,
|
||||
};
|
||||
}
|
||||
|
||||
async function renderFullBook() {
|
||||
const { text, chapterStarts, chapters, targets } = await assembleFullBook();
|
||||
const { text, chapterStarts, chapterEntries, chapters, targets } = await assembleFullBook();
|
||||
setEditorText(fullBookEditor, text);
|
||||
const ranges = chapterStarts.slice(1).map((from) => chapterBreakLine.range(from));
|
||||
fullBookEditor.dispatch({
|
||||
|
|
@ -584,6 +688,12 @@ export function initBook(api: FolioAPI) {
|
|||
});
|
||||
fullBookStats.textContent = `words: ${countWords(text)} · chars: ${text.length}`;
|
||||
printBook.innerHTML = renderPrintChapters(chapters, targets);
|
||||
populateChapterNav(chapterEntries);
|
||||
// Re-render replaces the whole document, which resets the scroll position,
|
||||
// so re-derive the current chapter once layout has settled.
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => setCurrentChapter(detectChapterFromScroll()));
|
||||
});
|
||||
}
|
||||
|
||||
async function selectFullBook() {
|
||||
|
|
@ -887,6 +997,12 @@ export function initBook(api: FolioAPI) {
|
|||
exportBtn.addEventListener("click", () => void exportMarkdown());
|
||||
exportZipBtn.addEventListener("click", () => void exportZip());
|
||||
fullBookBtn.addEventListener("click", () => void selectFullBook());
|
||||
fbPrevBtn.addEventListener("click", () => jumpToChapter(currentChapterIndex - 1));
|
||||
fbNextBtn.addEventListener("click", () => jumpToChapter(currentChapterIndex + 1));
|
||||
fbChapterSelect.addEventListener("change", () => {
|
||||
const idx = fullBookChapters.findIndex((c) => c.id === fbChapterSelect.value);
|
||||
if (idx >= 0) jumpToChapter(idx);
|
||||
});
|
||||
exportPdfBtn.addEventListener("click", () => void exportPdf());
|
||||
copyAllBtn.addEventListener("click", async () => {
|
||||
const text = editorText(fullBookEditor);
|
||||
|
|
|
|||
|
|
@ -508,6 +508,33 @@ html, body {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ---- full book chapter navigation ---- */
|
||||
.full-book-navbar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 8px 20px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-panel);
|
||||
}
|
||||
|
||||
.full-book-navbar .button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fb-nav-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fb-chapter-select {
|
||||
max-width: 280px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
/* ---- full book chapter breaks ----
|
||||
The Full Book stays one continuous scroll on screen; every chapter after
|
||||
the first gets a subtle separator where a printed page will begin. The
|
||||
|
|
@ -876,6 +903,10 @@ body.focus-mode .chapter-header {
|
|||
display: none;
|
||||
}
|
||||
|
||||
body.focus-mode .full-book-navbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.focus-mode .editor {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,6 +178,12 @@
|
|||
<span class="save-state" id="fullBookStatus"></span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="full-book-navbar" id="fullBookNavbar">
|
||||
<button class="button" id="fbPrevBtn" title="Jump to the previous chapter">← Previous Chapter</button>
|
||||
<label class="fb-nav-label" for="fbChapterSelect">Chapter:</label>
|
||||
<select id="fbChapterSelect" class="toolbar-select fb-chapter-select" title="Jump to a chapter"></select>
|
||||
<button class="button" id="fbNextBtn" title="Jump to the next chapter">Next Chapter →</button>
|
||||
</div>
|
||||
<div class="editor-panes" id="fullBookHost"></div>
|
||||
</div>
|
||||
<div class="editor-placeholder hidden" id="editorPlaceholder">
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@
|
|||
set -e
|
||||
BOOK=$(node -e 'const fs=require("fs"),path=require("path"),os=require("os");const d=fs.mkdtempSync(path.join(os.tmpdir(),"folio-e2e-"));fs.mkdirSync(path.join(d,"chapters"),{recursive:true});fs.writeFileSync(path.join(d,"folio.json"),JSON.stringify({app:"folio",version:1,title:"E2E Book",chapterOrder:[],chapters:{}}));process.stdout.write(d);')
|
||||
BOOK2=$(node -e 'const fs=require("fs"),path=require("path"),os=require("os");const d=fs.mkdtempSync(path.join(os.tmpdir(),"folio-e2e2-"));fs.mkdirSync(path.join(d,"chapters"),{recursive:true});fs.writeFileSync(path.join(d,"folio.json"),JSON.stringify({app:"folio",version:1,title:"E2E Book 2",chapterOrder:[],chapters:{}}));process.stdout.write(d);')
|
||||
BOOK3=$(node -e 'const fs=require("fs"),path=require("path"),os=require("os");const d=fs.mkdtempSync(path.join(os.tmpdir(),"folio-e2e3-"));fs.mkdirSync(path.join(d,"chapters"),{recursive:true});const titles=["One","Two","Three","Four"];const chapters={};const order=[];titles.forEach((t,i)=>{const id="ch"+(i+1);const file=path.posix.join("chapters",id+".md");order.push(id);chapters[id]={id,title:t,file};const body=Array.from({length:40},(_,p)=>`Paragraph ${p} of chapter ${t}: the quick brown fox jumps over the lazy dog while the whole manuscript stays deterministic and long enough to scroll.`).join("\n\n");fs.writeFileSync(path.join(d,file),`# ${t}\n\n${body}`);});fs.writeFileSync(path.join(d,"folio.json"),JSON.stringify({app:"folio",version:1,title:"Nav Book",chapterOrder:order,chapters}));process.stdout.write(d);')
|
||||
export FOLIO_SELF_TEST=1
|
||||
export FOLIO_TEST_BOOK="$BOOK"
|
||||
export FOLIO_TEST_BOOK2="$BOOK2"
|
||||
export FOLIO_TEST_BOOK3="$BOOK3"
|
||||
export FOLIO_TEST_PDF="$BOOK/export-test.pdf"
|
||||
trap 'rm -rf "$BOOK" "$BOOK2"' EXIT
|
||||
trap 'rm -rf "$BOOK" "$BOOK2" "$BOOK3"' EXIT
|
||||
xvfb-run -a electron .
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
renderer -> preload -> IPC -> chapters chain. */
|
||||
const BOOK_DIR = __BOOKDIR_VALUE__;
|
||||
const BOOK2_DIR = __BOOKDIR2_VALUE__;
|
||||
const BOOK3_DIR = __BOOKDIR3_VALUE__;
|
||||
(async () => {
|
||||
const out = {};
|
||||
try {
|
||||
|
|
@ -526,6 +527,193 @@ const BOOK2_DIR = __BOOKDIR2_VALUE__;
|
|||
out.printFmtWikilink &&
|
||||
out.printFmtNoRawWikilink;
|
||||
|
||||
// --- chapter navigation inside Full Book (third book: One/Two/Three/Four) ---
|
||||
// Four chapters, each taller than the viewport, so every jump target lands
|
||||
// its own heading near the top of the Full Book viewport.
|
||||
await window.folio.openBookPath(BOOK3_DIR);
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
const navBook = window.__folioFullBookEditor;
|
||||
const navViewEl = document.getElementById("fullBookView");
|
||||
const navSel = document.getElementById("fbChapterSelect");
|
||||
const navPrev = document.getElementById("fbPrevBtn");
|
||||
const navNext = document.getElementById("fbNextBtn");
|
||||
document.getElementById("fullBookBtn").click();
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
const navOptions = () => Array.from(navSel.options).map((o) => o.textContent);
|
||||
const navDocStarts = () => {
|
||||
const doc = navBook.state.doc.toString();
|
||||
return ["One", "Two", "Three", "Four"].map((t) => doc.indexOf("# " + t));
|
||||
};
|
||||
// How far (px) the given chapter's heading sits below the top of the Full
|
||||
// Book viewport. The jump is considered correct when it is near the top.
|
||||
const chapterTopY = (title) => {
|
||||
const doc = navBook.state.doc.toString();
|
||||
const start = doc.indexOf("# " + title);
|
||||
if (start < 0) return -1;
|
||||
const rect = navBook.scrollDOM.getBoundingClientRect();
|
||||
const c = navBook.coordsAtPos(start);
|
||||
if (!c) return -1;
|
||||
return c.top - rect.top;
|
||||
};
|
||||
const navTopChapter = () => {
|
||||
const scroller = navBook.scrollDOM;
|
||||
const rect = scroller.getBoundingClientRect();
|
||||
const pos = navBook.posAtCoords({ x: rect.left + 4, y: rect.top + 30 });
|
||||
if (pos === null || pos === undefined) return -1;
|
||||
const starts = navDocStarts();
|
||||
let idx = 0;
|
||||
for (let i = 0; i < starts.length; i++) {
|
||||
if (starts[i] >= 0 && starts[i] <= pos) idx = i;
|
||||
else break;
|
||||
}
|
||||
return idx;
|
||||
};
|
||||
const navTopLine = () => {
|
||||
const scroller = navBook.scrollDOM;
|
||||
const rect = scroller.getBoundingClientRect();
|
||||
const pos = navBook.posAtCoords({ x: rect.left + 4, y: rect.top + 30 });
|
||||
if (pos === null || pos === undefined) return "";
|
||||
return navBook.state.doc.lineAt(pos).text;
|
||||
};
|
||||
const navSnap = () => ({
|
||||
value: navSel.value,
|
||||
prevDisabled: navPrev.disabled,
|
||||
nextDisabled: navNext.disabled,
|
||||
topChapter: navTopChapter(),
|
||||
topLine: navTopLine(),
|
||||
fullBookVisible:
|
||||
!navViewEl.classList.contains("hidden") &&
|
||||
document.getElementById("chapterView").classList.contains("hidden"),
|
||||
});
|
||||
const navWait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// 1. Full Book opens at Chapter One; Previous disabled.
|
||||
const navStart = navSnap();
|
||||
out.navOptions = navOptions();
|
||||
out.navOptionsOk =
|
||||
JSON.stringify(out.navOptions) === JSON.stringify(["One", "Two", "Three", "Four"]);
|
||||
out.navOpenAtOne = navStart;
|
||||
out.navOpenAtOneOk =
|
||||
navStart.value === "ch1" &&
|
||||
navStart.prevDisabled &&
|
||||
!navStart.nextDisabled &&
|
||||
navStart.fullBookVisible &&
|
||||
navStart.topChapter === 0;
|
||||
|
||||
// 2. Next -> Chapter Two.
|
||||
navNext.click();
|
||||
await navWait(300);
|
||||
const navTwo = navSnap();
|
||||
out.navNextToTwo = navTwo;
|
||||
out.navNextToTwoOk =
|
||||
navTwo.value === "ch2" &&
|
||||
!navTwo.prevDisabled &&
|
||||
!navTwo.nextDisabled &&
|
||||
navTwo.fullBookVisible &&
|
||||
navTwo.topChapter === 1 &&
|
||||
chapterTopY("Two") >= 0 &&
|
||||
chapterTopY("Two") < 200;
|
||||
|
||||
// 3. Next -> Chapter Three.
|
||||
navNext.click();
|
||||
await navWait(300);
|
||||
const navThree = navSnap();
|
||||
out.navNextToThree = navThree;
|
||||
out.navNextToThreeOk =
|
||||
navThree.value === "ch3" &&
|
||||
navThree.fullBookVisible &&
|
||||
navThree.topChapter === 2 &&
|
||||
chapterTopY("Three") >= 0 &&
|
||||
chapterTopY("Three") < 200;
|
||||
|
||||
// 4. Previous -> back to Chapter Two.
|
||||
navPrev.click();
|
||||
await navWait(300);
|
||||
const navBackTwo = navSnap();
|
||||
out.navPrevToTwo = navBackTwo;
|
||||
out.navPrevToTwoOk =
|
||||
navBackTwo.value === "ch2" &&
|
||||
navBackTwo.topChapter === 1 &&
|
||||
chapterTopY("Two") >= 0 &&
|
||||
chapterTopY("Two") < 200;
|
||||
|
||||
// 5. Dropdown -> Chapter Four (Next disabled at the last chapter).
|
||||
navSel.value = "ch4";
|
||||
navSel.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await navWait(300);
|
||||
const navFour = navSnap();
|
||||
out.navDropToFour = navFour;
|
||||
out.navDropToFourOk =
|
||||
navFour.value === "ch4" &&
|
||||
navFour.nextDisabled &&
|
||||
navFour.fullBookVisible &&
|
||||
chapterTopY("Four") >= 0 &&
|
||||
chapterTopY("Four") < 200;
|
||||
|
||||
// 6. Manual scrolling updates the selector: scroll to the very bottom
|
||||
// (last chapter) and to the very top (first chapter).
|
||||
navBook.scrollDOM.scrollTop = navBook.scrollDOM.scrollHeight;
|
||||
navBook.scrollDOM.dispatchEvent(new Event("scroll"));
|
||||
await navWait(250);
|
||||
const navScrolledBottom = navSnap();
|
||||
out.navScrollBottomTracks =
|
||||
navScrolledBottom.value === "ch4" && navScrolledBottom.nextDisabled;
|
||||
navBook.scrollDOM.scrollTop = 0;
|
||||
navBook.scrollDOM.dispatchEvent(new Event("scroll"));
|
||||
await navWait(250);
|
||||
const navScrolledTop = navSnap();
|
||||
out.navScrollTopTracks = navScrolledTop.value === "ch1" && navScrolledTop.prevDisabled;
|
||||
|
||||
// 7. Reordering chapters in the sidebar must be reflected immediately, even
|
||||
// while Full Book stays open (no restart). Drag the last row onto the first.
|
||||
const navRows = document.getElementById("chapterList").querySelectorAll(".chapter-row");
|
||||
const navDt = new DataTransfer();
|
||||
navRows[3].dispatchEvent(new DragEvent("dragstart", { dataTransfer: navDt, bubbles: true }));
|
||||
navRows[0].dispatchEvent(new DragEvent("dragover", { dataTransfer: navDt, bubbles: true }));
|
||||
navRows[0].dispatchEvent(new DragEvent("drop", { dataTransfer: navDt, bubbles: true }));
|
||||
navRows[3].dispatchEvent(new DragEvent("dragend", { dataTransfer: navDt, bubbles: true }));
|
||||
await navWait(400);
|
||||
const navOrderReordered = (await window.folio.getBookMeta()).chapterOrder;
|
||||
out.navOrderReordered = navOrderReordered;
|
||||
out.navReorderChanged =
|
||||
JSON.stringify(navOrderReordered) === JSON.stringify(["ch4", "ch1", "ch2", "ch3"]);
|
||||
const navOptionsAfter = navOptions();
|
||||
out.navOptionsAfterReorder = navOptionsAfter;
|
||||
out.navDropdownFollowsReorder =
|
||||
JSON.stringify(navOptionsAfter) === JSON.stringify(["Four", "One", "Two", "Three"]);
|
||||
|
||||
// Full Book is re-rendered at the top: the new first chapter (Four) is
|
||||
// current, Previous is disabled, and Next now moves to One.
|
||||
const navAfterReorder = navSnap();
|
||||
out.navAfterReorder = navAfterReorder;
|
||||
out.navFollowsNewOrder =
|
||||
navAfterReorder.value === "ch4" &&
|
||||
navAfterReorder.prevDisabled &&
|
||||
navAfterReorder.fullBookVisible &&
|
||||
chapterTopY("Four") >= 0 &&
|
||||
chapterTopY("Four") < 200;
|
||||
navNext.click();
|
||||
await navWait(300);
|
||||
const navNextAfterReorder = navSnap();
|
||||
out.navNextAfterReorder = navNextAfterReorder;
|
||||
out.navNextAfterReorderOk =
|
||||
navNextAfterReorder.value === "ch1" &&
|
||||
navNextAfterReorder.fullBookVisible &&
|
||||
chapterTopY("One") >= 0 &&
|
||||
chapterTopY("One") < 200;
|
||||
|
||||
// 8. Page breaks still render after navigation. Break markers are
|
||||
// decorations on the first line of every chapter after the first, and
|
||||
// CodeMirror only puts rendered lines in the DOM, so navigate to a middle
|
||||
// chapter and confirm its marked first line is present.
|
||||
navSel.value = "ch2";
|
||||
navSel.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await navWait(300);
|
||||
const navBreaks = chapterBreakLines();
|
||||
out.navBreakCount = navBreaks.length;
|
||||
out.navBreaksOk = navBreaks.length >= 1 && navBreaks.some((el) => el.textContent.startsWith("# Two"));
|
||||
|
||||
const okFlags = [
|
||||
"dndMoved",
|
||||
"kbMoved",
|
||||
|
|
@ -580,6 +768,19 @@ const BOOK2_DIR = __BOOKDIR2_VALUE__;
|
|||
"printBookLongFull",
|
||||
"pdfLongExportOk",
|
||||
"printFmtOk",
|
||||
"navOptionsOk",
|
||||
"navOpenAtOneOk",
|
||||
"navNextToTwoOk",
|
||||
"navNextToThreeOk",
|
||||
"navPrevToTwoOk",
|
||||
"navDropToFourOk",
|
||||
"navScrollBottomTracks",
|
||||
"navScrollTopTracks",
|
||||
"navReorderChanged",
|
||||
"navDropdownFollowsReorder",
|
||||
"navFollowsNewOrder",
|
||||
"navNextAfterReorderOk",
|
||||
"navBreaksOk",
|
||||
];
|
||||
out.all = okFlags.every((f) => out[f] === true);
|
||||
} catch (e) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue