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:
John Factotum
2023-03-28 16:01:51 +08:00
parent 6f29a12426
commit 7d721611fa
3 changed files with 10 additions and 118 deletions
-28
View File
@@ -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 => {
-63
View 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 }
}
+10 -27
View File
@@ -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