Skip to content

Instantly share code, notes, and snippets.

@barneycarroll
Last active December 12, 2017 12:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save barneycarroll/365870576c4748bd1d60e481c0a23c2b to your computer and use it in GitHub Desktop.
Save barneycarroll/365870576c4748bd1d60e481c0a23c2b to your computer and use it in GitHub Desktop.
War on scrolling! Paginate web pages and use arrow keys or a small UI to navigate between screens
const $spans = [...document.querySelectorAll('*')]
.map($ =>
[...$.childNodes].filter($ =>
$.nodeType === 3
)
)
.reduce((a, b) =>
[...a, ...b]
)
.map(textNode => {
const $span = document.createElement('span')
$span.appendChild(textNode.cloneNode())
textNode.replaceWith($span)
return $span
})
const [[margin]] = $spans
.map($ =>
$.getBoundingClientRect().left
)
.sort((a, b) =>
a > b ? 1 : -1
)
.reduce(
([last, ...previous], current) =>
[
...(
current === last[0]
?
[[current, ...last]]
:
[[current], last]
),
...previous
],
[[]]
)
.sort((a, b) =>
a.length < b.length ? 1 : -1
)
$spans.forEach($ =>
$.replaceWith($.firstChild)
)
const pages = [...document.querySelectorAll('*')]
.filter($ =>
getComputedStyle($).display === 'block'
&&
$.offsetHeight < innerHeight
)
.reduce(
($$, $, i) => (
i && $$[$$.length - 1].contains($)
?
$$
:
[...$$, $]
),
[]
)
.map($ => ({
$,
top: $.offsetTop,
bottom: $.offsetTop + $.offsetHeight,
}))
.reduce(
({pages, offset}, {$, top, bottom}) => {
const edge = pages * innerHeight,
over = edge <= bottom + offset,
inter = edge - margin <= offset + bottom
&&
edge + margin >= offset + top
if(over || inter)
pages++
if(inter){
const $gap = document.createElement('div')
const gap = edge - top + margin
$gap.style.height = gap + 'px'
$.prepend($gap)
offset += gap
}
return {
pages,
offset,
}
},
{
pages: 1,
offset: 0,
}
)
.pages - 1
document.head.appendChild(
Object.assign(
document.createElement('script'),
{
src: 'https://unpkg.com/mithril',
onload: () => {
let page = 0
Object.assign(document.body.style, {
overflow: 'hidden',
height: innerHeight * pages + 'px',
})
const back = () =>
m.redraw(page && page--)
const forð = () =>
m.redraw(page < pages && page++)
document.addEventListener('keydown', ({key}) =>
key === 'ArrowLeft' && back()
||
key === 'ArrowRight' && forð()
)
m.mount(
document.body.appendChild(
document.createElement('div')
),
{
view : () => (
scrollTo(0, page * innerHeight),
m('div', {
style : {
background: 'hsla(0,0,100,80)',
position: 'fixed',
bottom: 0,
right: 0,
zIndex: 9999,
},
},
m('button', {onclick : back}, '<'),
' ',
m('span', page, '/', pages),
' ',
m('button', {onclick : forð}, '>')
)
)
}
)
}
}
)
)
@barneycarroll
Copy link
Author

Revision 1

Works in principle but flawed in practice.

  1. The page break location algorithm sometimes screws up and introduces breaks progressively too soon.
  2. The page break elements sometimes get introduced in awkward places, for example between the beginning of a styled block quote container and its text.

The first suggests a browser error which baulks at repeatedly querying the rendered position of many block elements, and prematurely over-optimises the box dimension queries to all take place without re-calculating the layout. This is a forgivable optimisation, inasmuch as thrashing the DOM (reading from computed properties, influencing said properties through DOM modification, in a loop) is grossly inefficient. This can in fact be optimised in this code by simply getting all computed properties in one frame, and storing the cumulative offset introduced by gaps.

The second point is trickier and the symptom will probably always appear in some cases according to the general method of introducing gaps as DOM elements in strategic places. At face value the particular bug described is a result of only processing leaf node block elements (blocks which contain no other blocks): the block element containing the text has the gap inserted before it when the desired location of the gap would be before a higher order containing block element. It's difficult to know how to include these in the processing algorithm without undesirably including many higher order block elements and their children (eg a naive 'fix' would also include the body itself, which necessarily breaks across all pages, meaning an infinite loop of introduce this element on the next page). Other similar complications in this method will invariably arise :/

An alternative theoretical method for avoiding the problem of where to introduce page break gaps as elements in the DOM might conceivably involve using CSS columns to determine column breaks procedurally, by making the single lowest common ancestor have an excessive width measure, a height fixed at viewport height and a column-width fixed at viewport width : the number of pages would then be determined by the rightmost edge of all contained spans; the page navigation mechanism would then involve translating the container horizontally by X viewports. It remains to be seen how else this might produce undesirable DOM layout artefacts.

@barneycarroll
Copy link
Author

CSS columns seems like a non-starter.

@barneycarroll
Copy link
Author

…So the question of how to fix #2 now is how to identify more appropriate higher order containing blocks without including unhelpfully large ones. Perhaps prioritise higher order containers by default unless they exceed viewport height?

@barneycarroll
Copy link
Author

Revision 2

Untested attempt at addressing the bugs using latest proposed solutions. Accidentally deleted UI code — Github Gists aren't friendly on mobile.

Tweaked page counting algorithm to account for instances where the page may break naturally in the pre-existing gap between 2 blocks — in practice this will break the mechanism because A) it doesn't account for offset and B) it won't pick up on introduced gaps as a result of margin intersection.

@barneycarroll
Copy link
Author

Revision 3

Fixed the blind-coding errors but now the gaps mechanism is broken 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment