mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29: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 { View, getPosition } from './view.js'
|
||||||
import { createTOCView } from './ui/tree.js'
|
import { createTOCView } from './ui/tree.js'
|
||||||
import { createMenu } from './ui/menu.js'
|
import { createMenu } from './ui/menu.js'
|
||||||
import { createPopover } from './ui/popover.js'
|
|
||||||
|
|
||||||
const isZip = async file => {
|
const isZip = async file => {
|
||||||
const arr = new Uint8Array(await file.slice(0, 4).arrayBuffer())
|
const arr = new Uint8Array(await file.slice(0, 4).arrayBuffer())
|
||||||
@@ -245,8 +244,6 @@ class Reader {
|
|||||||
switch (obj.type) {
|
switch (obj.type) {
|
||||||
case 'loaded': this.#onLoaded(obj); break
|
case 'loaded': this.#onLoaded(obj); break
|
||||||
case 'relocated': this.#onRelocated(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) {
|
#handleKeydown(event) {
|
||||||
@@ -269,31 +266,6 @@ class Reader {
|
|||||||
slider.title = `${percent} · ${loc}`
|
slider.title = `${percent} · ${loc}`
|
||||||
if (tocItem?.href) this.#tocView?.setCurrentHref?.(tocItem.href)
|
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 => {
|
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
|
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 {
|
export class View {
|
||||||
#sectionProgress
|
#sectionProgress
|
||||||
#tocProgress
|
#tocProgress
|
||||||
@@ -150,8 +141,12 @@ export class View {
|
|||||||
doc.documentElement.dir ||= this.isCJK ? '' : this.textDirection
|
doc.documentElement.dir ||= this.isCJK ? '' : this.textDirection
|
||||||
|
|
||||||
this.renderer.setStyle?.(this.#css)
|
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]
|
const section = book.sections[index]
|
||||||
for (const a of doc.querySelectorAll('a[href]'))
|
for (const a of doc.querySelectorAll('a[href]'))
|
||||||
a.addEventListener('click', e => {
|
a.addEventListener('click', e => {
|
||||||
@@ -159,25 +154,13 @@ export class View {
|
|||||||
const href = a.getAttribute('href')
|
const href = a.getAttribute('href')
|
||||||
const uri = section?.resolveHref?.(href) ?? href
|
const uri = section?.resolveHref?.(href) ?? href
|
||||||
if (book?.isExternal?.(uri))
|
if (book?.isExternal?.(uri))
|
||||||
this.emit?.({ type: 'external-link', uri })
|
Promise.resolve(emit?.({ type: 'external-link', a, uri }))
|
||||||
else if (SSV.isRef(a)) {
|
.then(x => x ? null : window.open(uri, '_blank'))
|
||||||
const { index, anchor } = book.resolveHref(uri)
|
.catch(e => console.error(e))
|
||||||
Promise.resolve(book.sections[index].createDocument())
|
else Promise.resolve(emit?.({ type: 'link', a, uri }))
|
||||||
.then(doc => [anchor(doc), doc.contentType])
|
.then(x => x ? null : this.goTo(uri))
|
||||||
.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)
|
|
||||||
.catch(e => console.error(e))
|
.catch(e => console.error(e))
|
||||||
return
|
|
||||||
} else this.goTo(uri)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.emit?.({ type: 'loaded', doc, index })
|
|
||||||
}
|
}
|
||||||
async addAnnotation(annotation, remove) {
|
async addAnnotation(annotation, remove) {
|
||||||
const { value } = annotation
|
const { value } = annotation
|
||||||
|
|||||||
Reference in New Issue
Block a user