mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 03:19:14 -04:00
Improve handling of links
- Remove special handling of footnotes - Open all links by default - Reader can return `true` to prevent default action - Expose link handling as a method, which the reader can use to set up links outside renderers (e.g. in popup footnotes)
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { View, getPosition } from './view.js'
|
||||
import { createTOCView } from './ui/tree.js'
|
||||
import { createMenu } from './ui/menu.js'
|
||||
import { createPopover } from './ui/popover.js'
|
||||
|
||||
const isZip = async file => {
|
||||
const arr = new Uint8Array(await file.slice(0, 4).arrayBuffer())
|
||||
@@ -245,8 +244,6 @@ class Reader {
|
||||
switch (obj.type) {
|
||||
case 'loaded': this.#onLoaded(obj); break
|
||||
case 'relocated': this.#onRelocated(obj); break
|
||||
case 'reference': this.#onReference(obj); break
|
||||
case 'external-link': globalThis.open(obj.uri, '_blank'); break
|
||||
}
|
||||
}
|
||||
#handleKeydown(event) {
|
||||
@@ -269,31 +266,6 @@ class Reader {
|
||||
slider.title = `${percent} · ${loc}`
|
||||
if (tocItem?.href) this.#tocView?.setCurrentHref?.(tocItem.href)
|
||||
}
|
||||
#onReference(obj) {
|
||||
const { content, element } = obj
|
||||
const { point, dir } = getPosition(element)
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.sandbox = 'allow-same-origin'
|
||||
iframe.srcdoc = content
|
||||
iframe.onload = () => {
|
||||
const doc = iframe.contentDocument
|
||||
doc.documentElement.style.colorScheme = 'light dark'
|
||||
doc.body.style.margin = '18px'
|
||||
}
|
||||
Object.assign(iframe.style, {
|
||||
border: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
})
|
||||
const { popover, arrow, overlay } = createPopover(300, 250, point, dir)
|
||||
overlay.style.zIndex = 3
|
||||
popover.style.zIndex = 3
|
||||
arrow.style.zIndex = 3
|
||||
popover.append(iframe)
|
||||
document.body.append(overlay)
|
||||
document.body.append(popover)
|
||||
document.body.append(arrow)
|
||||
}
|
||||
}
|
||||
|
||||
const open = async file => {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
const clamp = (min, max, x) => Math.min(max, Math.max(min, x))
|
||||
|
||||
const arrowHeight = 10
|
||||
const arrowWidth = 20
|
||||
const radius = 6
|
||||
|
||||
const createSVGElement = tag =>
|
||||
document.createElementNS('http://www.w3.org/2000/svg', tag)
|
||||
|
||||
const createArrow = down => {
|
||||
const h = arrowHeight + 1
|
||||
const svg = createSVGElement('svg')
|
||||
svg.setAttribute('aria-hidden', 'true')
|
||||
svg.setAttribute('width', arrowWidth)
|
||||
svg.setAttribute('height', arrowHeight)
|
||||
const polygon = createSVGElement('polygon')
|
||||
polygon.setAttribute('points', down
|
||||
? `0 ${h}, ${arrowWidth / 2} 0, ${arrowWidth} ${h}`
|
||||
: `0 0, ${arrowWidth / 2} ${h}, ${arrowWidth} 0`)
|
||||
svg.classList.add(down ? 'popover-arrow-down' : 'popover-arrow-up')
|
||||
svg.append(polygon)
|
||||
return svg
|
||||
}
|
||||
|
||||
export const createPopover = (width, height, { x, y }, dir) => {
|
||||
const down = dir === 'down'
|
||||
const fullHeight = height + arrowHeight
|
||||
const overlay = document.createElement('div')
|
||||
Object.assign(overlay.style, {
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
})
|
||||
const arrow = createArrow(down)
|
||||
const top = clamp(0, window.innerHeight - (down ? height : fullHeight),
|
||||
down ? y + arrowHeight : y - fullHeight)
|
||||
Object.assign(arrow.style, {
|
||||
position: 'absolute',
|
||||
left: `${clamp(radius, window.innerWidth - arrowWidth - radius,
|
||||
x - arrowWidth / 2)}px`,
|
||||
top: `${down ? top - arrowHeight : top + height}px`,
|
||||
})
|
||||
const popover = document.createElement('div')
|
||||
popover.setAttribute('role', 'dialog')
|
||||
popover.classList.add('popover')
|
||||
Object.assign(popover.style, {
|
||||
position: 'absolute',
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'hidden',
|
||||
left: `${clamp(0, window.innerWidth - width, x - width / 2)}px`,
|
||||
top: `${top}px`,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
})
|
||||
overlay.addEventListener('click', () => {
|
||||
overlay.parentNode.removeChild(overlay)
|
||||
arrow.parentNode.removeChild(arrow)
|
||||
popover.parentNode.removeChild(popover)
|
||||
})
|
||||
return { popover, arrow, overlay }
|
||||
}
|
||||
@@ -66,15 +66,6 @@ export const getPosition = target => {
|
||||
return start.point.y > window.innerHeight - end.point.y ? start : end
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/epub-ssv-11/
|
||||
const SSV = Object.fromEntries(Array.from(Object.entries({
|
||||
isRef: ['annoref', 'biblioref', 'glossref', 'noteref'],
|
||||
isLink: ['backlink'],
|
||||
isNote: ['annotation', 'note', 'footnote', 'endnote', 'rearnote'],
|
||||
}), ([k, v]) => [k, el =>
|
||||
el.getAttributeNS('http://www.idpf.org/2007/ops', 'type')
|
||||
?.split(/s/)?.some(t => v.includes(t))]))
|
||||
|
||||
export class View {
|
||||
#sectionProgress
|
||||
#tocProgress
|
||||
@@ -150,8 +141,12 @@ export class View {
|
||||
doc.documentElement.dir ||= this.isCJK ? '' : this.textDirection
|
||||
|
||||
this.renderer.setStyle?.(this.#css)
|
||||
this.handleLinks(doc, index, this.emit)
|
||||
|
||||
// set handlers for links
|
||||
this.emit?.({ type: 'loaded', doc, index })
|
||||
}
|
||||
handleLinks(doc, index, emit) {
|
||||
const { book } = this
|
||||
const section = book.sections[index]
|
||||
for (const a of doc.querySelectorAll('a[href]'))
|
||||
a.addEventListener('click', e => {
|
||||
@@ -159,25 +154,13 @@ export class View {
|
||||
const href = a.getAttribute('href')
|
||||
const uri = section?.resolveHref?.(href) ?? href
|
||||
if (book?.isExternal?.(uri))
|
||||
this.emit?.({ type: 'external-link', uri })
|
||||
else if (SSV.isRef(a)) {
|
||||
const { index, anchor } = book.resolveHref(uri)
|
||||
Promise.resolve(book.sections[index].createDocument())
|
||||
.then(doc => [anchor(doc), doc.contentType])
|
||||
.then(([el, type]) =>
|
||||
[el?.innerHTML, type, SSV.isNote(el)])
|
||||
.then(([content, contentType, isNote]) => content
|
||||
? this.emit?.({
|
||||
type: 'reference',
|
||||
href: isNote ? null : uri,
|
||||
content, contentType, element: a,
|
||||
}) : null)
|
||||
Promise.resolve(emit?.({ type: 'external-link', a, uri }))
|
||||
.then(x => x ? null : window.open(uri, '_blank'))
|
||||
.catch(e => console.error(e))
|
||||
else Promise.resolve(emit?.({ type: 'link', a, uri }))
|
||||
.then(x => x ? null : this.goTo(uri))
|
||||
.catch(e => console.error(e))
|
||||
return
|
||||
} else this.goTo(uri)
|
||||
})
|
||||
|
||||
this.emit?.({ type: 'loaded', doc, index })
|
||||
}
|
||||
async addAnnotation(annotation, remove) {
|
||||
const { value } = annotation
|
||||
|
||||
Reference in New Issue
Block a user