mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
Reader: add layout option
This commit is contained in:
+64
-14
@@ -5,6 +5,16 @@
|
|||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' blob:; script-src 'self'; style-src 'self' blob: 'unsafe-inline'; img-src 'self' blob: data:; connect-src 'self' blob: data:; frame-src blob: data:; object-src 'none'; form-action 'none';">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self' blob:; script-src 'self'; style-src 'self' blob: 'unsafe-inline'; img-src 'self' blob: data:; connect-src 'self' blob: data:; frame-src blob: data:; object-src 'none'; form-action 'none';">
|
||||||
<title>E-Book Reader</title>
|
<title>E-Book Reader</title>
|
||||||
<style>
|
<style>
|
||||||
|
:root {
|
||||||
|
--active-bg: rgba(0, 0, 0, .05);
|
||||||
|
}
|
||||||
|
@supports (color-scheme: light dark) {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--active-bg: rgba(255, 255, 255, .1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font: menu;
|
font: menu;
|
||||||
@@ -47,21 +57,19 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
opacity: .25;
|
|
||||||
transition: opacity 250ms ease;
|
transition: opacity 250ms ease;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
.toolbar:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.toolbar button {
|
.toolbar button {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: none;
|
background: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
color: GrayText;
|
||||||
}
|
}
|
||||||
.toolbar button:hover {
|
.toolbar button:hover {
|
||||||
background: rgba(0, 0, 0, .1);
|
background: rgba(0, 0, 0, .1);
|
||||||
|
color: currentcolor;
|
||||||
}
|
}
|
||||||
#header-bar {
|
#header-bar {
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -72,6 +80,13 @@ body {
|
|||||||
#progress-label {
|
#progress-label {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: GrayText;
|
||||||
|
}
|
||||||
|
.scrolled #progress-label {
|
||||||
|
color: #fff;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 2em;
|
||||||
|
padding: 6px 12px;
|
||||||
}
|
}
|
||||||
#side-bar {
|
#side-bar {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@@ -143,14 +158,6 @@ body {
|
|||||||
#toc-view {
|
#toc-view {
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
--active-bg: rgba(0, 0, 0, .05);
|
|
||||||
}
|
|
||||||
@supports (color-scheme: light dark) {
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
#toc-view {
|
|
||||||
--active-bg: rgba(255, 255, 255, .1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#toc-view li, #toc-view ol {
|
#toc-view li, #toc-view ol {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -195,6 +202,41 @@ body {
|
|||||||
#toc-view [aria-expanded="false"] + [role="group"] {
|
#toc-view [aria-expanded="false"] + [role="group"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.menu-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.menu, .menu ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.menu {
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
background: Canvas;
|
||||||
|
color: CanvasText;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, .2), 0 0 16px rgba(0, 0, 0, .1);
|
||||||
|
padding: 6px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.menu.show {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.menu li {
|
||||||
|
padding: 6px 12px;
|
||||||
|
padding-left: 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.menu li:hover {
|
||||||
|
background: var(--active-bg);
|
||||||
|
}
|
||||||
|
.menu li[aria-checked="true"] {
|
||||||
|
background-position: center left;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%2212%22%20r%3D%223%22%2F%3E%3C%2Fsvg%3E');
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<input type="file" id="file-input" hidden>
|
<input type="file" id="file-input" hidden>
|
||||||
<div id="drop-target" class="filter">
|
<div id="drop-target" class="filter">
|
||||||
@@ -223,15 +265,23 @@ body {
|
|||||||
<path d="M 4 6 h 16 M 4 12 h 16 M 4 18 h 16"/>
|
<path d="M 4 6 h 16 M 4 12 h 16 M 4 18 h 16"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<div id="menu-button" class="menu-container">
|
||||||
|
<button aria-label="Show settings" aria-haspopup="true">
|
||||||
|
<svg class="icon" width="24" height="24" aria-hidden="true">
|
||||||
|
<path d="M5 12.7a7 7 0 0 1 0-1.4l-1.8-2 2-3.5 2.7.5a7 7 0 0 1 1.2-.7L10 3h4l.9 2.6 1.2.7 2.7-.5 2 3.4-1.8 2a7 7 0 0 1 0 1.5l1.8 2-2 3.5-2.7-.5a7 7 0 0 1-1.2.7L14 21h-4l-.9-2.6a7 7 0 0 1-1.2-.7l-2.7.5-2-3.4 1.8-2Z"/>
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="nav-bar" class="toolbar">
|
<div id="nav-bar" class="toolbar">
|
||||||
<button id="left-button" aria-label="Turn left page">
|
<button id="left-button" aria-label="Go left">
|
||||||
<svg class="icon" width="24" height="24" aria-hidden="true">
|
<svg class="icon" width="24" height="24" aria-hidden="true">
|
||||||
<path d="M 15 6 L 9 12 L 15 18"/>
|
<path d="M 15 6 L 9 12 L 15 18"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div id="progress-label"></div>
|
<div id="progress-label"></div>
|
||||||
<button id="right-button" aria-label="Turn right page">
|
<button id="right-button" aria-label="Go right">
|
||||||
<svg class="icon" width="24" height="24" aria-hidden="true">
|
<svg class="icon" width="24" height="24" aria-hidden="true">
|
||||||
<path d="M 9 6 L 15 12 L 9 18"/>
|
<path d="M 9 6 L 15 12 L 9 18"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* global zip: false, fflate: false */
|
/* global zip: false, fflate: false */
|
||||||
import { View } from './view.js'
|
import { View } from './view.js'
|
||||||
import { createTOCView } from './ui/tree.js'
|
import { createTOCView } from './ui/tree.js'
|
||||||
|
import { createMenu } from './ui/menu.js'
|
||||||
|
|
||||||
const { ZipReader, BlobReader, TextWriter, BlobWriter } = zip
|
const { ZipReader, BlobReader, TextWriter, BlobWriter } = zip
|
||||||
zip.configure({ useWebWorkers: false })
|
zip.configure({ useWebWorkers: false })
|
||||||
@@ -129,7 +130,17 @@ const open = async file => {
|
|||||||
justify: true,
|
justify: true,
|
||||||
hyphenate: true,
|
hyphenate: true,
|
||||||
}
|
}
|
||||||
view.setAppearance({ css: getCSS(style) })
|
const layout = {
|
||||||
|
margin: 48,
|
||||||
|
gap: 48,
|
||||||
|
maxColumnWidth: 720,
|
||||||
|
}
|
||||||
|
const setAppearance = () => {
|
||||||
|
view.setAppearance({ css: getCSS(style), layout })
|
||||||
|
const scrolled = layout.flow === 'scrolled'
|
||||||
|
document.documentElement.classList.toggle('scrolled', scrolled)
|
||||||
|
}
|
||||||
|
setAppearance()
|
||||||
view.renderer.next()
|
view.renderer.next()
|
||||||
|
|
||||||
const { book } = view
|
const { book } = view
|
||||||
@@ -171,6 +182,28 @@ const open = async file => {
|
|||||||
$('#nav-bar').style.visibility = 'visible'
|
$('#nav-bar').style.visibility = 'visible'
|
||||||
$('#left-button').addEventListener('click', () => view.goLeft())
|
$('#left-button').addEventListener('click', () => view.goLeft())
|
||||||
$('#right-button').addEventListener('click', () => view.goRight())
|
$('#right-button').addEventListener('click', () => view.goRight())
|
||||||
|
|
||||||
|
const menu = createMenu([
|
||||||
|
{
|
||||||
|
name: 'layout',
|
||||||
|
label: 'Layout',
|
||||||
|
type: 'radio',
|
||||||
|
items: [
|
||||||
|
['Paginated', 'paginated'],
|
||||||
|
['Scrolled', 'scrolled'],
|
||||||
|
],
|
||||||
|
onclick: value => {
|
||||||
|
layout.flow = value
|
||||||
|
setAppearance()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
])
|
||||||
|
menu.element.classList.add('menu')
|
||||||
|
|
||||||
|
$('#menu-button').append(menu.element)
|
||||||
|
$('#menu-button > button').addEventListener('click', () =>
|
||||||
|
menu.element.classList.toggle('show'))
|
||||||
|
menu.groups.layout.select('paginated')
|
||||||
}
|
}
|
||||||
|
|
||||||
const dragOverHandler = e => e.preventDefault()
|
const dragOverHandler = e => e.preventDefault()
|
||||||
|
|||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
const createMenuItemRadioGroup = (label, arr, onclick) => {
|
||||||
|
const group = document.createElement('ul')
|
||||||
|
group.setAttribute('role', 'group')
|
||||||
|
group.setAttribute('aria-label', label)
|
||||||
|
const map = new Map()
|
||||||
|
const select = value => {
|
||||||
|
onclick(value)
|
||||||
|
const item = map.get(value)
|
||||||
|
for (const child of group.children)
|
||||||
|
child.setAttribute('aria-checked', child === item ? 'true' : 'false')
|
||||||
|
}
|
||||||
|
for (const [label, value] of arr) {
|
||||||
|
const item = document.createElement('li')
|
||||||
|
item.setAttribute('role', 'menuitemradio')
|
||||||
|
item.innerText = label
|
||||||
|
item.onclick = () => select(value)
|
||||||
|
map.set(value, item)
|
||||||
|
group.append(item)
|
||||||
|
}
|
||||||
|
return { element: group, select }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMenu = arr => {
|
||||||
|
const groups = {}
|
||||||
|
const element = document.createElement('ul')
|
||||||
|
element.setAttribute('role', 'menu')
|
||||||
|
const hide = () => element.classList.remove('show')
|
||||||
|
const hideAnd = f => (...args) => (hide(), f(...args))
|
||||||
|
for (const { name, label, type, items, onclick } of arr) {
|
||||||
|
const widget = type === 'radio'
|
||||||
|
? createMenuItemRadioGroup(label, items, hideAnd(onclick))
|
||||||
|
: null
|
||||||
|
if (name) groups[name] = widget
|
||||||
|
element.append(widget.element)
|
||||||
|
}
|
||||||
|
// TODO: keyboard events
|
||||||
|
window.addEventListener('blur', () => hide())
|
||||||
|
window.addEventListener('click', e => {
|
||||||
|
if (!element.parentNode.contains(e.target)) hide()
|
||||||
|
})
|
||||||
|
return { element, groups }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user