mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(studio): give a book its first and its last page (#2317)
A new book was a cover, a spread and a back cover, so it opened straight onto pages 2 and 3. A bound book does not: the inside of the cover is blank, and the first thing facing the reader is a single right-hand page, page 1. The same at the end, where a single left-hand page sits before the back cover. Both are laid down now, empty, in a new book and in a laid-out one, and they are fixed the way the covers are: an inner spread only ever goes between them. The page numbers follow. They were counted from a spread's position in the array, which assumed the spread after the cover was the first, and three places did that sum on their own. One function now counts along the book, one page for a single leaf and two for a spread, so the sheet on screen and the label in the print view and the folio on the page all say the same number. A book with a first page starts at 1; a book from before the first page existed has none and keeps the numbers it always had.
This commit is contained in:
@@ -178,9 +178,9 @@ describe('an entry with words but no pictures', () => {
|
||||
})
|
||||
|
||||
describe('a journey with nothing at all', () => {
|
||||
it('produces a book of a cover and a back, and no blank pages between', () => {
|
||||
it('produces a book of the covers and the single pages inside them, and no spreads between', () => {
|
||||
const spreads = build([])
|
||||
expect(inner(spreads)).toHaveLength(0)
|
||||
expect(spreads.map(s => s.role)).toEqual(['cover', 'back'])
|
||||
expect(spreads.map(s => s.role)).toEqual(['cover', 'first', 'last', 'back'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -83,10 +83,11 @@ describe('the summary spread', () => {
|
||||
expect(has(doc, 'stats')).toBe(true)
|
||||
})
|
||||
|
||||
it('comes straight after the cover, before the entries', () => {
|
||||
it('comes straight after the cover and the first page, before the entries', () => {
|
||||
const doc = build()
|
||||
expect(doc.spreads[0].role).toBe('cover')
|
||||
expect(kinds(doc.spreads[1])).toContain('map')
|
||||
expect(doc.spreads[1].role).toBe('first')
|
||||
expect(kinds(doc.spreads[2])).toContain('map')
|
||||
})
|
||||
|
||||
it('carries the journey figures rather than placeholders', () => {
|
||||
@@ -125,8 +126,8 @@ describe('the summary spread', () => {
|
||||
points: [], distance: 0, days: 0, steps: 0, photos: 0, places: 0, furthest: 0, countries: [],
|
||||
}),
|
||||
})
|
||||
// Cover, the one entry, back cover — no summary, no country page.
|
||||
expect(doc.spreads.map(sp => sp.role)).toEqual(['cover', 'inner', 'back'])
|
||||
// Cover, first page, the one entry, last page, back cover — no summary, no country page.
|
||||
expect(doc.spreads.map(sp => sp.role)).toEqual(['cover', 'first', 'inner', 'last', 'back'])
|
||||
expect(has(doc, 'map')).toBe(false)
|
||||
expect(has(doc, 'stats')).toBe(false)
|
||||
})
|
||||
@@ -136,8 +137,8 @@ describe('the summary spread', () => {
|
||||
expect(has(doc, 'map')).toBe(false)
|
||||
expect(has(doc, 'stats')).toBe(false)
|
||||
expect(has(doc, 'countries')).toBe(false)
|
||||
// And the book still builds: cover, the entry, back cover.
|
||||
expect(doc.spreads).toHaveLength(3)
|
||||
// And the book still builds: cover, first page, the entry, last page, back cover.
|
||||
expect(doc.spreads).toHaveLength(5)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -175,7 +176,7 @@ describe('the country page', () => {
|
||||
it('sits between the summary and the entries', () => {
|
||||
const doc = build({ stats: twoCountries })
|
||||
const order = doc.spreads.map(sp => kinds(sp).find(k => k === 'map' || k === 'countries') ?? sp.role)
|
||||
expect(order.slice(0, 3)).toEqual(['cover', 'map', 'countries'])
|
||||
expect(order.slice(0, 4)).toEqual(['cover', 'first', 'map', 'countries'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -313,13 +314,37 @@ describe('an empty book', () => {
|
||||
stats: stats(),
|
||||
}
|
||||
|
||||
it('has a cover, a page and a back cover, and nothing on any of them', () => {
|
||||
it('has a cover, a first page, a spread, a last page and a back cover, and nothing on any of them', () => {
|
||||
const doc = emptyBook(input)
|
||||
expect(doc.spreads.map(s => s.role)).toEqual(['cover', 'inner', 'back'])
|
||||
expect(doc.spreads.map(s => s.role)).toEqual(['cover', 'first', 'inner', 'last', 'back'])
|
||||
expect(doc.spreads.every(s => s.elements.length === 0)).toBe(true)
|
||||
expect(doc.spreads.every(s => s.background === null)).toBe(true)
|
||||
})
|
||||
|
||||
/*
|
||||
* A bound book opens onto page 1, the single right-hand page behind the
|
||||
* cover (#2317). The default setup counts from 2, which was right while the
|
||||
* book opened straight onto a spread; a book laid down with a first page
|
||||
* counts from that page instead, whatever setup it was handed.
|
||||
*/
|
||||
it('numbers its first page 1, whatever the setup said', () => {
|
||||
expect(input.page.pageNumbers.startAt).toBe(2)
|
||||
expect(emptyBook(input).page.pageNumbers.startAt).toBe(1)
|
||||
expect(buildBook(input).page.pageNumbers.startAt).toBe(1)
|
||||
// Nothing else of the setup moves.
|
||||
expect(emptyBook(input).page.pageNumbers.position).toBe(input.page.pageNumbers.position)
|
||||
})
|
||||
|
||||
it('lays the first and last pages down empty in a laid-out book too', () => {
|
||||
const doc = buildBook(input)
|
||||
const first = doc.spreads.find(s => s.role === 'first')!
|
||||
const last = doc.spreads.find(s => s.role === 'last')!
|
||||
expect(doc.spreads.indexOf(first)).toBe(1)
|
||||
expect(doc.spreads.indexOf(last)).toBe(doc.spreads.length - 2)
|
||||
expect(first.elements).toEqual([])
|
||||
expect(last.elements).toEqual([])
|
||||
})
|
||||
|
||||
it('carries the journey title and the page setup it was given', () => {
|
||||
const doc = emptyBook(input)
|
||||
expect(doc.title).toBe('Iceland')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import type { BookDocument, BookSpread } from '@trek/shared'
|
||||
import { bookPageSetupSchema } from '@trek/shared'
|
||||
import { MARK_LENGTH, sheetBox, sheetsFor } from '../../../src/components/Studio/bookSheets'
|
||||
import { MARK_LENGTH, folioLabel, foliosOf, isNumbered, sheetBox, sheetsFor } from '../../../src/components/Studio/bookSheets'
|
||||
|
||||
/**
|
||||
* Cutting a book into sheets (#1973).
|
||||
@@ -117,3 +117,75 @@ describe('an empty book', () => {
|
||||
expect(sheetsFor(book([]), 'pages')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* A bound book opens onto one right-hand page and closes on one left-hand
|
||||
* page (#2317). They are single leaves like the covers, but unlike the covers
|
||||
* they are numbered — and the numbers count along the book, so a book with
|
||||
* them and a book from before them both read right.
|
||||
*/
|
||||
describe('the first and last pages', () => {
|
||||
it('stay whole in both modes, like the covers', () => {
|
||||
const roles: BookSpread['role'][] = ['cover', 'first', 'inner', 'last', 'back']
|
||||
for (const mode of ['pages', 'spreads'] as const) {
|
||||
const sheets = sheetsFor(book(roles), mode)
|
||||
const firstSheet = sheets.find(s => s.spread.role === 'first')!
|
||||
const lastSheet = sheets.find(s => s.spread.role === 'last')!
|
||||
expect(firstSheet.single, mode).toBe(true)
|
||||
expect(firstSheet.width, mode).toBe(210)
|
||||
expect(lastSheet.single, mode).toBe(true)
|
||||
expect(lastSheet.width, mode).toBe(210)
|
||||
}
|
||||
})
|
||||
|
||||
it('are numbered, counting from startAt along the book', () => {
|
||||
const roles: BookSpread['role'][] = ['cover', 'first', 'inner', 'inner', 'last', 'back']
|
||||
const sheets = sheetsFor(book(roles, { pageNumbers: { startAt: 1 } }), 'pages')
|
||||
expect(sheets.map(s => s.label)).toEqual(['', '1', '2', '3', '4', '5', '6', ''])
|
||||
expect(sheetsFor(book(roles, { pageNumbers: { startAt: 1 } }), 'spreads').map(s => s.label))
|
||||
.toEqual(['', '1', '2 – 3', '4 – 5', '6', ''])
|
||||
})
|
||||
|
||||
it('leave a book without them on the numbers it always had', () => {
|
||||
// startAt 2, no first page: the first spread still opens on 2 – 3.
|
||||
const sheets = sheetsFor(book(['cover', 'inner', 'inner', 'back']), 'spreads')
|
||||
expect(sheets.map(s => s.label)).toEqual(['', '2 – 3', '4 – 5', ''])
|
||||
})
|
||||
})
|
||||
|
||||
describe('foliosOf', () => {
|
||||
const roles: BookSpread['role'][] = ['cover', 'first', 'inner', 'inner', 'last', 'back']
|
||||
const spreads = book(roles).spreads
|
||||
|
||||
it('hands a cover nothing, a single page one number and a spread two', () => {
|
||||
expect(foliosOf(spreads, 0, 1)).toEqual([])
|
||||
expect(foliosOf(spreads, 1, 1)).toEqual([1])
|
||||
expect(foliosOf(spreads, 2, 1)).toEqual([2, 3])
|
||||
expect(foliosOf(spreads, 3, 1)).toEqual([4, 5])
|
||||
expect(foliosOf(spreads, 4, 1)).toEqual([6])
|
||||
expect(foliosOf(spreads, 5, 1)).toEqual([])
|
||||
})
|
||||
|
||||
it('starts wherever the book says', () => {
|
||||
expect(foliosOf(spreads, 1, 3)).toEqual([3])
|
||||
expect(foliosOf(spreads, 2, 3)).toEqual([4, 5])
|
||||
})
|
||||
|
||||
it('is empty past the end of the book', () => {
|
||||
expect(foliosOf(spreads, 99, 1)).toEqual([])
|
||||
})
|
||||
|
||||
it('numbers everything inside the covers', () => {
|
||||
expect(isNumbered('cover')).toBe(false)
|
||||
expect(isNumbered('back')).toBe(false)
|
||||
expect(isNumbered('first')).toBe(true)
|
||||
expect(isNumbered('inner')).toBe(true)
|
||||
expect(isNumbered('last')).toBe(true)
|
||||
})
|
||||
|
||||
it('labels a pair with a dash, one number plainly, and nothing as nothing', () => {
|
||||
expect(folioLabel([2, 3])).toBe('2 – 3')
|
||||
expect(folioLabel([7])).toBe('7')
|
||||
expect(folioLabel([])).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -56,48 +56,62 @@ describe('a custom trim size', () => {
|
||||
describe('page numbers', () => {
|
||||
it('are off until asked for', () => {
|
||||
expect(page().pageNumbers.show).toBe(false)
|
||||
const { container } = render(<SpreadView spread={spread()} page={page()} spreadIndex={1} />)
|
||||
const { container } = render(<SpreadView spread={spread()} page={page()} folios={[2, 3]} />)
|
||||
expect(container.textContent).toBe('')
|
||||
})
|
||||
|
||||
it('number both pages of a spread, left then right', () => {
|
||||
const { container } = render(
|
||||
<SpreadView spread={spread()} page={page({ pageNumbers: { show: true } })} spreadIndex={1} />,
|
||||
<SpreadView spread={spread()} page={page({ pageNumbers: { show: true } })} folios={[2, 3]} />,
|
||||
)
|
||||
expect(container.textContent).toBe('23')
|
||||
})
|
||||
|
||||
/*
|
||||
* The first inner spread opens on `startAt`, and the cover is a separate
|
||||
* sheet — which is why the index has to be offset rather than used raw.
|
||||
* The numbers come from the caller (foliosOf, tested with bookSheets): the
|
||||
* sheet draws whatever it is handed, so a spread deep in the book reads
|
||||
* what the book says it is.
|
||||
*/
|
||||
it('advance by two for each spread further into the book', () => {
|
||||
it('draw the numbers they are handed, wherever the spread sits', () => {
|
||||
const p = page({ pageNumbers: { show: true } })
|
||||
const third = render(<SpreadView spread={spread()} page={p} spreadIndex={3} />)
|
||||
const third = render(<SpreadView spread={spread()} page={p} folios={[6, 7]} />)
|
||||
expect(third.container.textContent).toBe('67')
|
||||
})
|
||||
|
||||
it('start where the book says, since a binder may or may not count the cover', () => {
|
||||
const p = page({ pageNumbers: { show: true, startAt: 1 } })
|
||||
const { container } = render(<SpreadView spread={spread()} page={p} spreadIndex={1} />)
|
||||
expect(container.textContent).toBe('12')
|
||||
})
|
||||
|
||||
/* A folio on a cover is a mistake in every book ever bound. */
|
||||
it('never appear on the cover or the back cover', () => {
|
||||
const p = page({ pageNumbers: { show: true } })
|
||||
for (const role of ['cover', 'back'] as const) {
|
||||
const { container } = render(<SpreadView spread={spread({ role })} page={p} spreadIndex={0} />)
|
||||
const { container } = render(<SpreadView spread={spread({ role })} page={p} folios={[1]} />)
|
||||
expect(container.textContent, role).toBe('')
|
||||
}
|
||||
})
|
||||
|
||||
/* The single first page is a right-hand leaf and the last a left-hand one (#2317). */
|
||||
it('put one number on a first or last page, on the side it is bound', () => {
|
||||
const p = page({ pageNumbers: { show: true, position: 'outer' } })
|
||||
const first = render(<SpreadView spread={spread({ role: 'first' })} page={p} folios={[1]} />)
|
||||
const last = render(<SpreadView spread={spread({ role: 'last' })} page={p} folios={[9]} />)
|
||||
expect(first.container.textContent).toBe('1')
|
||||
expect(last.container.textContent).toBe('9')
|
||||
const box = (c: HTMLElement) => (c.firstElementChild!.children[0]) as HTMLElement
|
||||
// Outer on a right-hand page is the right edge; on a left-hand page the left.
|
||||
expect(box(first.container).style.textAlign).toBe('right')
|
||||
expect(box(last.container).style.textAlign).toBe('left')
|
||||
})
|
||||
|
||||
it('draw nothing for a numbered page handed no numbers', () => {
|
||||
const p = page({ pageNumbers: { show: true } })
|
||||
const { container } = render(<SpreadView spread={spread()} page={p} folios={[]} />)
|
||||
expect(container.textContent).toBe('')
|
||||
})
|
||||
|
||||
it('sit against the cut edge on outer, and in the gutter on inner', () => {
|
||||
const outer = render(
|
||||
<SpreadView spread={spread()} page={page({ pageNumbers: { show: true, position: 'outer' } })} spreadIndex={1} />,
|
||||
<SpreadView spread={spread()} page={page({ pageNumbers: { show: true, position: 'outer' } })} folios={[2, 3]} />,
|
||||
)
|
||||
const inner = render(
|
||||
<SpreadView spread={spread()} page={page({ pageNumbers: { show: true, position: 'inner' } })} spreadIndex={1} />,
|
||||
<SpreadView spread={spread()} page={page({ pageNumbers: { show: true, position: 'inner' } })} folios={[2, 3]} />,
|
||||
)
|
||||
const leftOf = (c: HTMLElement) =>
|
||||
parseFloat(((c.firstElementChild!.children[0]) as HTMLElement).style.left)
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('an empty frame', () => {
|
||||
*/
|
||||
it('wears the frame it will wear once it is filled', () => {
|
||||
const { container } = render(
|
||||
<SpreadView spread={spread([frame({ frameStyle: 'polaroid' })])} page={page} spreadIndex={1} />,
|
||||
<SpreadView spread={spread([frame({ frameStyle: 'polaroid' })])} page={page} />,
|
||||
)
|
||||
const outer = container.querySelector<HTMLElement>('[style*="left: 20mm"]')!
|
||||
// A Polaroid's white card and the shadow of a print lying on a page.
|
||||
@@ -48,7 +48,7 @@ describe('an empty frame', () => {
|
||||
|
||||
it('shows a film frame as film, black card and all', () => {
|
||||
const { container } = render(
|
||||
<SpreadView spread={spread([frame({ frameStyle: 'film' })])} page={page} spreadIndex={1} />,
|
||||
<SpreadView spread={spread([frame({ frameStyle: 'film' })])} page={page} />,
|
||||
)
|
||||
const outer = container.querySelector<HTMLElement>('[style*="left: 20mm"]')!
|
||||
expect(outer.style.background).toContain('rgb(20, 20, 20)')
|
||||
@@ -56,7 +56,7 @@ describe('an empty frame', () => {
|
||||
|
||||
it('leaves a plain frame plain', () => {
|
||||
const { container } = render(
|
||||
<SpreadView spread={spread([frame()])} page={page} spreadIndex={1} />,
|
||||
<SpreadView spread={spread([frame()])} page={page} />,
|
||||
)
|
||||
const outer = container.querySelector<HTMLElement>('[style*="left: 20mm"]')!
|
||||
expect(outer.style.boxShadow).toBe('')
|
||||
@@ -65,7 +65,7 @@ describe('an empty frame', () => {
|
||||
/* In the bound book an unfilled frame is nothing at all. */
|
||||
it('prints as nothing', () => {
|
||||
const { container } = render(
|
||||
<SpreadView spread={spread([frame({ frameStyle: 'polaroid' })])} page={page} spreadIndex={1} print />,
|
||||
<SpreadView spread={spread([frame({ frameStyle: 'polaroid' })])} page={page} print />,
|
||||
)
|
||||
expect(container.querySelector('[style*="left: 20mm"]')).toBeNull()
|
||||
})
|
||||
|
||||
@@ -311,3 +311,66 @@ describe('turning an element', () => {
|
||||
expect(rotationOf('one')).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* The single first and last pages are fixed the way the covers are (#2317):
|
||||
* a bound book opens onto one right-hand page and closes on one left-hand
|
||||
* page, and an inner spread may only ever go between them.
|
||||
*/
|
||||
describe('a book with a first and a last page', () => {
|
||||
beforeEach(() => {
|
||||
useStudioStore.getState().load(book(
|
||||
spread('cover', 'cover'),
|
||||
spread('first', 'first'),
|
||||
spread('a', 'inner'),
|
||||
spread('last', 'last'),
|
||||
spread('back', 'back'),
|
||||
))
|
||||
})
|
||||
|
||||
it('treats them as fixed points, not as inner spreads', () => {
|
||||
expect(store().canEditSpread(1)).toBe(false)
|
||||
expect(store().canEditSpread(3)).toBe(false)
|
||||
expect(store().canEditSpread(2)).toBe(true)
|
||||
})
|
||||
|
||||
it('adds after the first page when asked to add at the cover or the first page', () => {
|
||||
store().addSpread(0)
|
||||
expect(roles()).toEqual(['cover', 'first', 'inner', 'inner', 'last', 'back'])
|
||||
expect(store().activeSpread).toBe(2)
|
||||
})
|
||||
|
||||
it('never lands on or behind the last page', () => {
|
||||
store().addSpread(3)
|
||||
expect(roles()).toEqual(['cover', 'first', 'inner', 'inner', 'last', 'back'])
|
||||
store().addSpread(9)
|
||||
expect(roles()).toEqual(['cover', 'first', 'inner', 'inner', 'inner', 'last', 'back'])
|
||||
})
|
||||
|
||||
it('inserts a spread inside them too', () => {
|
||||
store().insertSpread(0, spread('x', 'inner'))
|
||||
expect(roles()).toEqual(['cover', 'first', 'inner', 'inner', 'last', 'back'])
|
||||
})
|
||||
|
||||
it('will not move an inner spread onto either of them', () => {
|
||||
store().moveSpread(2, -1)
|
||||
store().moveSpread(2, 1)
|
||||
expect(ids()).toEqual(['cover', 'first', 'a', 'last', 'back'])
|
||||
})
|
||||
|
||||
it('will not move, copy or delete them', () => {
|
||||
store().moveSpread(1, 1)
|
||||
store().moveSpread(3, -1)
|
||||
store().duplicateSpread(1)
|
||||
store().removeSpread(3)
|
||||
expect(ids()).toEqual(['cover', 'first', 'a', 'last', 'back'])
|
||||
})
|
||||
|
||||
it('adds the first inner spread of an empty book between them', () => {
|
||||
useStudioStore.getState().load(book(
|
||||
spread('cover', 'cover'), spread('first', 'first'), spread('last', 'last'), spread('back', 'back'),
|
||||
))
|
||||
store().addSpread(-1)
|
||||
expect(roles()).toEqual(['cover', 'first', 'inner', 'last', 'back'])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user