mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
Add annotations
Simplify overlayer API; allow one overlayer only.
This commit is contained in:
@@ -87,7 +87,7 @@ It has two renderers, one for paginating reflowable books, and one for fixed-lay
|
|||||||
- `.book`: the book object that will be rendered.
|
- `.book`: the book object that will be rendered.
|
||||||
- `.onLoad(doc, index)`: callback when a section is loaded. Takes a `Document` object and the index of the section.
|
- `.onLoad(doc, index)`: callback when a section is loaded. Takes a `Document` object and the index of the section.
|
||||||
- `.onRelocated(range, index, fraction)`: callback when locations changes. `range` is a `Range` object containing the current visible area. `fraction` is a number between 0 and 1, representing the reading progress within the section.
|
- `.onRelocated(range, index, fraction)`: callback when locations changes. `range` is a `Range` object containing the current visible area. `fraction` is a number between 0 and 1, representing the reading progress within the section.
|
||||||
- `createOverlayers(doc, index)`: callback for adding overlays to the page. It should return an object whose property values are overlayer objects (see the description for `overlayer.js` below). The key names can be any string you want.
|
- `createOverlayer(doc, index)`: callback for adding an overlay to the page. It should return an overlayer object (see the description for `overlayer.js` below).
|
||||||
|
|
||||||
A renderer's interface is currently mainly:
|
A renderer's interface is currently mainly:
|
||||||
- `.element`: the DOM element of the renderer. It needs to be manually appended to the document by the consumer of the renderer.
|
- `.element`: the DOM element of the renderer. It needs to be manually appended to the document by the consumer of the renderer.
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
export class Annotations {
|
||||||
|
#annotationsByIndex = new Map()
|
||||||
|
#byValue = new Map()
|
||||||
|
#anchorsByValue = new Map()
|
||||||
|
#indicesByValue = new Map()
|
||||||
|
constructor({ resolve, compare, onAdd, onDelete, onUpdate }) {
|
||||||
|
this.resolve = resolve
|
||||||
|
this.compare = compare
|
||||||
|
this.onAdd = onAdd
|
||||||
|
this.onDelete = onDelete
|
||||||
|
this.onUpdate = onUpdate
|
||||||
|
}
|
||||||
|
async add(annotation, sorted) {
|
||||||
|
const { value } = annotation
|
||||||
|
if (this.#byValue.has(value)) return
|
||||||
|
const { index, anchor } = await this.resolve(value)
|
||||||
|
this.#byValue.set(value, annotation)
|
||||||
|
this.#indicesByValue.set(value, index)
|
||||||
|
this.#anchorsByValue.set(value, anchor)
|
||||||
|
if (this.#annotationsByIndex.has(index)) {
|
||||||
|
const arr = this.#annotationsByIndex.get(index)
|
||||||
|
if (sorted) {
|
||||||
|
arr.push(annotation)
|
||||||
|
this.onAdd?.(annotation, index, arr.length - 1)
|
||||||
|
} else {
|
||||||
|
let position = 0
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
const itemValue = arr[i].value
|
||||||
|
if (this.compare(value, itemValue) <= 0) break
|
||||||
|
position = i + 1
|
||||||
|
}
|
||||||
|
arr.splice(position, 0, annotation)
|
||||||
|
this.onAdd?.(annotation, index, position)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.#annotationsByIndex.set(index, [annotation])
|
||||||
|
this.onAdd?.(annotation, index, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update(annotation) {
|
||||||
|
const index = this.#indicesByValue.get(annotation.value)
|
||||||
|
const old = this.#byValue.get(annotation.value)
|
||||||
|
Object.assign(old, annotation)
|
||||||
|
this.onUpdate?.(annotation, index)
|
||||||
|
}
|
||||||
|
delete(value) {
|
||||||
|
const index = this.#indicesByValue.get(value)
|
||||||
|
const arr = this.#annotationsByIndex.get(index)
|
||||||
|
const position = arr.findIndex(a => a.value === value)
|
||||||
|
arr.splice(position, 1)
|
||||||
|
this.#byValue.delete(value)
|
||||||
|
this.#indicesByValue.delete(value)
|
||||||
|
this.#anchorsByValue.delete(value)
|
||||||
|
this.onDelete?.(value, index, position)
|
||||||
|
}
|
||||||
|
getByIndex(index) {
|
||||||
|
return this.#annotationsByIndex.get(index) ?? []
|
||||||
|
}
|
||||||
|
getAnchor(value) {
|
||||||
|
return this.#anchorsByValue.get(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-24
@@ -124,7 +124,7 @@ class View {
|
|||||||
#element = document.createElement('div')
|
#element = document.createElement('div')
|
||||||
#iframe = document.createElement('iframe')
|
#iframe = document.createElement('iframe')
|
||||||
#contentRange = document.createRange()
|
#contentRange = document.createRange()
|
||||||
#overlayers = {}
|
#overlayer
|
||||||
#vertical = false
|
#vertical = false
|
||||||
#rtl = false
|
#rtl = false
|
||||||
#column = true
|
#column = true
|
||||||
@@ -273,10 +273,10 @@ class View {
|
|||||||
this.#element.style[side] = `${expandedSize}px`
|
this.#element.style[side] = `${expandedSize}px`
|
||||||
this.#iframe.style[otherSide] = '100%'
|
this.#iframe.style[otherSide] = '100%'
|
||||||
this.#element.style[otherSide] = '100%'
|
this.#element.style[otherSide] = '100%'
|
||||||
for (const overlayer of Object.values(this.#overlayers)) {
|
if (this.#overlayer) {
|
||||||
overlayer.element.style.margin = '0'
|
this.#overlayer.element.style.margin = '0'
|
||||||
overlayer.element.style[side] = `${expandedSize}px`
|
this.#overlayer.element.style[side] = `${expandedSize}px`
|
||||||
overlayer.redraw()
|
this.#overlayer.redraw()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const side = this.#vertical ? 'width' : 'height'
|
const side = this.#vertical ? 'width' : 'height'
|
||||||
@@ -291,20 +291,19 @@ class View {
|
|||||||
this.#element.style[side] = `${expandedSize}px`
|
this.#element.style[side] = `${expandedSize}px`
|
||||||
this.#iframe.style[otherSide] = '100%'
|
this.#iframe.style[otherSide] = '100%'
|
||||||
this.#element.style[otherSide] = '100%'
|
this.#element.style[otherSide] = '100%'
|
||||||
for (const overlayer of Object.values(this.#overlayers)) {
|
if (this.#overlayer) {
|
||||||
overlayer.element.style.margin = padding
|
this.#overlayer.element.style.margin = padding
|
||||||
overlayer.element.style[side] = `${expandedSize}px`
|
this.#overlayer.element.style[side] = `${expandedSize}px`
|
||||||
overlayer.redraw()
|
this.#overlayer.redraw()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set overlayers(overlayers) {
|
set overlayer(overlayer) {
|
||||||
this.#overlayers = overlayers
|
this.#overlayer = overlayer
|
||||||
for (const overlayer of Object.values(overlayers))
|
|
||||||
this.#element.append(overlayer.element)
|
this.#element.append(overlayer.element)
|
||||||
}
|
}
|
||||||
get overlayers() {
|
get overlayer() {
|
||||||
return this.#overlayers
|
return this.#overlayer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,11 +322,11 @@ export class Paginator {
|
|||||||
gap: 40,
|
gap: 40,
|
||||||
maxColumnWidth: 700,
|
maxColumnWidth: 700,
|
||||||
}
|
}
|
||||||
constructor({ book, onLoad, onRelocated, createOverlayers }) {
|
constructor({ book, onLoad, onRelocated, createOverlayer }) {
|
||||||
this.sections = book.sections
|
this.sections = book.sections
|
||||||
this.onLoad = onLoad
|
this.onLoad = onLoad
|
||||||
this.onRelocated = onRelocated
|
this.onRelocated = onRelocated
|
||||||
this.createOverlayers = createOverlayers
|
this.createOverlayer = createOverlayer
|
||||||
Object.assign(this.#element.style, {
|
Object.assign(this.#element.style, {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'nowrap',
|
flexWrap: 'nowrap',
|
||||||
@@ -533,8 +532,8 @@ export class Paginator {
|
|||||||
}
|
}
|
||||||
const beforeRender = this.#beforeRender.bind(this)
|
const beforeRender = this.#beforeRender.bind(this)
|
||||||
await view.load(src, afterLoad, beforeRender)
|
await view.load(src, afterLoad, beforeRender)
|
||||||
const overlayers = this.createOverlayers?.(view.document, index)
|
const overlayer = this.createOverlayer?.(view.document, index)
|
||||||
if (overlayers) view.overlayers = overlayers
|
if (overlayer) view.overlayer = overlayer
|
||||||
this.#view = view
|
this.#view = view
|
||||||
}
|
}
|
||||||
this.#anchor = (typeof anchor === 'function'
|
this.#anchor = (typeof anchor === 'function'
|
||||||
@@ -630,13 +629,12 @@ export class Paginator {
|
|||||||
const index = this.sections.findLastIndex(section => section.linear !== 'no')
|
const index = this.sections.findLastIndex(section => section.linear !== 'no')
|
||||||
return this.goTo({ index })
|
return this.goTo({ index })
|
||||||
}
|
}
|
||||||
getOverlayers() {
|
getOverlayer() {
|
||||||
if (!this.#view) return []
|
if (this.#view) return {
|
||||||
return [{
|
|
||||||
index: this.#index,
|
index: this.#index,
|
||||||
overlayers: this.#view.overlayers,
|
overlayer: this.#view.overlayer,
|
||||||
document: this.#view.document,
|
doc: this.#view.document,
|
||||||
}]
|
}
|
||||||
}
|
}
|
||||||
setStyle(style) {
|
setStyle(style) {
|
||||||
const $style = this.#styleMap.get(this.#view?.document)
|
const $style = this.#styleMap.get(this.#view?.document)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import * as CFI from './epubcfi.js'
|
import * as CFI from './epubcfi.js'
|
||||||
import { TOCProgress, SectionProgress } from './progress.js'
|
import { TOCProgress, SectionProgress } from './progress.js'
|
||||||
|
import { Overlayer } from './overlayer.js'
|
||||||
|
import { Annotations } from './annotations.js'
|
||||||
|
|
||||||
const textWalker = function* (doc, func) {
|
const textWalker = function* (doc, func) {
|
||||||
const filter = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT
|
const filter = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT
|
||||||
@@ -73,11 +75,32 @@ export class View {
|
|||||||
#sectionProgress
|
#sectionProgress
|
||||||
#tocProgress
|
#tocProgress
|
||||||
#pageProgress
|
#pageProgress
|
||||||
|
#css
|
||||||
language = 'en'
|
language = 'en'
|
||||||
textDirection = ''
|
textDirection = ''
|
||||||
isCJK = false
|
isCJK = false
|
||||||
isFixedLayout = false
|
isFixedLayout = false
|
||||||
#css
|
annotations = new Annotations({
|
||||||
|
resolve: value => this.resolveCFI(value),
|
||||||
|
compare: CFI.compare,
|
||||||
|
onAdd: (annotation, index, position) => {
|
||||||
|
const o = this.#getOverlayer(index)
|
||||||
|
if (o) this.#drawAnnotation(o.doc, o.overlayer, annotation)
|
||||||
|
const label = this.#tocProgress.getProgress(index)?.label ?? ''
|
||||||
|
this?.emit({ type: 'add-annotation', annotation, label, index, position })
|
||||||
|
},
|
||||||
|
onDelete: (key, index, position) => {
|
||||||
|
this.#getOverlayer(index)?.overlayer?.remove(key)
|
||||||
|
this?.emit({ type: 'delete-annotation', index, position })
|
||||||
|
},
|
||||||
|
onUpdate: (annotation, index) => {
|
||||||
|
const o = this.#getOverlayer(index)
|
||||||
|
if (o) {
|
||||||
|
o.overlayer.remove(annotation.value)
|
||||||
|
this.#drawAnnotation(o.doc, o.overlayer, annotation)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
constructor(book, emit) {
|
constructor(book, emit) {
|
||||||
this.book = book
|
this.book = book
|
||||||
this.emit = emit
|
this.emit = emit
|
||||||
@@ -109,6 +132,7 @@ export class View {
|
|||||||
book: this.book,
|
book: this.book,
|
||||||
onLoad: this.#onLoad.bind(this),
|
onLoad: this.#onLoad.bind(this),
|
||||||
onRelocated: this.#onRelocated.bind(this),
|
onRelocated: this.#onRelocated.bind(this),
|
||||||
|
createOverlayer: this.#createOverlayer.bind(this),
|
||||||
}
|
}
|
||||||
this.isFixedLayout = this.book.rendition?.layout === 'pre-paginated'
|
this.isFixedLayout = this.book.rendition?.layout === 'pre-paginated'
|
||||||
if (this.isFixedLayout) {
|
if (this.isFixedLayout) {
|
||||||
@@ -120,6 +144,19 @@ export class View {
|
|||||||
}
|
}
|
||||||
return this.renderer.element
|
return this.renderer.element
|
||||||
}
|
}
|
||||||
|
async init({ lastLocation, annotations }) {
|
||||||
|
if (lastLocation) {
|
||||||
|
const resolved = this.resolveNavigation(lastLocation)
|
||||||
|
if (resolved) await this.renderer.goTo(resolved)
|
||||||
|
else await this.renderer.next()
|
||||||
|
} else await this.renderer.next()
|
||||||
|
|
||||||
|
if (annotations) {
|
||||||
|
annotations.sort((a, b) => CFI.compare(a.value, b.value))
|
||||||
|
for (const annotation of annotations)
|
||||||
|
await this.annotations.add(annotation, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
#onRelocated(range, index, fraction) {
|
#onRelocated(range, index, fraction) {
|
||||||
if (!this.#sectionProgress) return
|
if (!this.#sectionProgress) return
|
||||||
const progress = this.#sectionProgress.getProgress(index, fraction)
|
const progress = this.#sectionProgress.getProgress(index, fraction)
|
||||||
@@ -175,6 +212,38 @@ export class View {
|
|||||||
|
|
||||||
this.emit?.({ type: 'loaded', doc })
|
this.emit?.({ type: 'loaded', doc })
|
||||||
}
|
}
|
||||||
|
#drawAnnotation(doc, overlayer, annotation) {
|
||||||
|
const { value } = annotation
|
||||||
|
const anchor = this.annotations.getAnchor(value)
|
||||||
|
const range = doc ? anchor(doc) : anchor
|
||||||
|
const [func, opts] = this.emit({ type: 'draw-annotation', annotation })
|
||||||
|
overlayer.add(value, range, func, opts)
|
||||||
|
}
|
||||||
|
#getOverlayer(index) {
|
||||||
|
const obj = this.renderer.getOverlayer()
|
||||||
|
if (obj.index === index) return obj
|
||||||
|
}
|
||||||
|
#createOverlayer(doc, index) {
|
||||||
|
const overlayer = new Overlayer()
|
||||||
|
for (const annotation of this.annotations.getByIndex(index))
|
||||||
|
this.#drawAnnotation(doc, overlayer, annotation)
|
||||||
|
doc.addEventListener('click', e => {
|
||||||
|
const [value, range] = overlayer.hitTest(e)
|
||||||
|
if (value) {
|
||||||
|
const pos = getPosition(range)
|
||||||
|
this.emit?.({ type: 'show-annotation', value, pos })
|
||||||
|
}
|
||||||
|
}, false)
|
||||||
|
return overlayer
|
||||||
|
}
|
||||||
|
async showAnnotation(annotation) {
|
||||||
|
const { value } = annotation
|
||||||
|
const { index, anchor } = await this.goTo(value)
|
||||||
|
const { doc } = this.#getOverlayer(index)
|
||||||
|
const range = anchor(doc)
|
||||||
|
const pos = getPosition(range)
|
||||||
|
this.emit?.({ type: 'show-annotation', value, pos })
|
||||||
|
}
|
||||||
getCFI(index, range) {
|
getCFI(index, range) {
|
||||||
if (!range) return ''
|
if (!range) return ''
|
||||||
const baseCFI = this.book.sections[index].cfi ?? CFI.fake.fromIndex(index)
|
const baseCFI = this.book.sections[index].cfi ?? CFI.fake.fromIndex(index)
|
||||||
|
|||||||
Reference in New Issue
Block a user