diff --git a/README.md b/README.md index 997f041..874b3f8 100644 --- a/README.md +++ b/README.md @@ -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. - `.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. -- `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: - `.element`: the DOM element of the renderer. It needs to be manually appended to the document by the consumer of the renderer. diff --git a/annotations.js b/annotations.js new file mode 100644 index 0000000..3d92795 --- /dev/null +++ b/annotations.js @@ -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) + } +} diff --git a/paginator.js b/paginator.js index 692b816..49ecd7d 100644 --- a/paginator.js +++ b/paginator.js @@ -124,7 +124,7 @@ class View { #element = document.createElement('div') #iframe = document.createElement('iframe') #contentRange = document.createRange() - #overlayers = {} + #overlayer #vertical = false #rtl = false #column = true @@ -273,10 +273,10 @@ class View { this.#element.style[side] = `${expandedSize}px` this.#iframe.style[otherSide] = '100%' this.#element.style[otherSide] = '100%' - for (const overlayer of Object.values(this.#overlayers)) { - overlayer.element.style.margin = '0' - overlayer.element.style[side] = `${expandedSize}px` - overlayer.redraw() + if (this.#overlayer) { + this.#overlayer.element.style.margin = '0' + this.#overlayer.element.style[side] = `${expandedSize}px` + this.#overlayer.redraw() } } else { const side = this.#vertical ? 'width' : 'height' @@ -291,20 +291,19 @@ class View { this.#element.style[side] = `${expandedSize}px` this.#iframe.style[otherSide] = '100%' this.#element.style[otherSide] = '100%' - for (const overlayer of Object.values(this.#overlayers)) { - overlayer.element.style.margin = padding - overlayer.element.style[side] = `${expandedSize}px` - overlayer.redraw() + if (this.#overlayer) { + this.#overlayer.element.style.margin = padding + this.#overlayer.element.style[side] = `${expandedSize}px` + this.#overlayer.redraw() } } } - set overlayers(overlayers) { - this.#overlayers = overlayers - for (const overlayer of Object.values(overlayers)) - this.#element.append(overlayer.element) + set overlayer(overlayer) { + this.#overlayer = overlayer + this.#element.append(overlayer.element) } - get overlayers() { - return this.#overlayers + get overlayer() { + return this.#overlayer } } @@ -323,11 +322,11 @@ export class Paginator { gap: 40, maxColumnWidth: 700, } - constructor({ book, onLoad, onRelocated, createOverlayers }) { + constructor({ book, onLoad, onRelocated, createOverlayer }) { this.sections = book.sections this.onLoad = onLoad this.onRelocated = onRelocated - this.createOverlayers = createOverlayers + this.createOverlayer = createOverlayer Object.assign(this.#element.style, { display: 'flex', flexWrap: 'nowrap', @@ -533,8 +532,8 @@ export class Paginator { } const beforeRender = this.#beforeRender.bind(this) await view.load(src, afterLoad, beforeRender) - const overlayers = this.createOverlayers?.(view.document, index) - if (overlayers) view.overlayers = overlayers + const overlayer = this.createOverlayer?.(view.document, index) + if (overlayer) view.overlayer = overlayer this.#view = view } this.#anchor = (typeof anchor === 'function' @@ -630,13 +629,12 @@ export class Paginator { const index = this.sections.findLastIndex(section => section.linear !== 'no') return this.goTo({ index }) } - getOverlayers() { - if (!this.#view) return [] - return [{ + getOverlayer() { + if (this.#view) return { index: this.#index, - overlayers: this.#view.overlayers, - document: this.#view.document, - }] + overlayer: this.#view.overlayer, + doc: this.#view.document, + } } setStyle(style) { const $style = this.#styleMap.get(this.#view?.document) diff --git a/view.js b/view.js index 2c39401..b7b6d06 100644 --- a/view.js +++ b/view.js @@ -1,5 +1,7 @@ import * as CFI from './epubcfi.js' import { TOCProgress, SectionProgress } from './progress.js' +import { Overlayer } from './overlayer.js' +import { Annotations } from './annotations.js' const textWalker = function* (doc, func) { const filter = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT @@ -73,11 +75,32 @@ export class View { #sectionProgress #tocProgress #pageProgress + #css language = 'en' textDirection = '' isCJK = 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) { this.book = book this.emit = emit @@ -109,6 +132,7 @@ export class View { book: this.book, onLoad: this.#onLoad.bind(this), onRelocated: this.#onRelocated.bind(this), + createOverlayer: this.#createOverlayer.bind(this), } this.isFixedLayout = this.book.rendition?.layout === 'pre-paginated' if (this.isFixedLayout) { @@ -120,6 +144,19 @@ export class View { } 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) { if (!this.#sectionProgress) return const progress = this.#sectionProgress.getProgress(index, fraction) @@ -175,6 +212,38 @@ export class View { 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) { if (!range) return '' const baseCFI = this.book.sections[index].cfi ?? CFI.fake.fromIndex(index)