futurepress / epub.js

Enhanced eBooks in the browser.
http://futurepress.org
Other
6.46k stars 1.11k forks source link

CFI to position #809

Open ncammarata opened 6 years ago

ncammarata commented 6 years ago

I would like to implement annotations a la Google Docs where you can just scroll the range into view. However, this requires me to convert from cfi -> position on screen. Is this possible?

ncammarata commented 6 years ago

looks like https://github.com/futurepress/epub.js/issues/728 solves it!

viking2917 commented 6 years ago

weird bit of synchronicity, I am trying to solve exactly this problem right now too!

However, #728 doesn't seem to work for me, I get:

console.log(range)
VM7985:1 Range {startContainer: text, startOffset: 70, endContainer: text, endOffset: 197, collapsed: false, …}
undefined
range.getClientBoundingRect();
VM7987:1 Uncaught TypeError: range.getClientBoundingRect is not a function
    at eval (eval at $scope.onRenditionSelected (angularEpubReader.js:280), <anonymous>:1:7)
    at Rendition.$scope.onRenditionSelected (angularEpubReader.js:280)
    at Rendition.emit (epubnew.js:2152)
    at Rendition.triggerSelectedEvent (epubnew.js:7403)
    at Contents.<anonymous> (epubnew.js:7371)
    at Contents.emit (epubnew.js:2160)
    at Contents.triggerSelectedEvent (epubnew.js:4847)
    at Contents.<anonymous> (epubnew.js:4828)
(anonymous) @ VM7987:1
$scope.onRenditionSelected @ angularEpubReader.js:280
emit @ epubnew.js:2152
triggerSelectedEvent @ epubnew.js:7403
(anonymous) @ epubnew.js:7371
emit @ epubnew.js:2160
triggerSelectedEvent @ epubnew.js:4847
(anonymous) @ epubnew.js:4828
setTimeout (async)
onSelectionChange @ epubnew.js:4826
viking2917 commented 6 years ago

Whoops. I was using a older/different version of epub.js. This works fine in the "highlight" example from the repo.

ncammarata commented 6 years ago

it turns out that this gives the position relative to the iframe, not relative to the page. For the Google docs-type experience, I need it relative to the page. Would love help here if anyone knows a way!

dmisdm commented 6 years ago

had the same issue! the way i solved it was by maintaining a reference on my state to the latest view.element emitted by the rendition rendered event, then using that as an offset against range.getBoundingClientRect(). Im pretty sure thats the only way to do it, as the latest "rendered" will definitely contain the view element that is visible to the user (pretty sure). So to solve your problem you might do something like this:

let cfiRange = "epubcfi(....)";
let latestViewElement;
rendition.on("rendered", (section, view) => {
  latestViewElement = view.element;
});

const getCfiRangeBounds = () => {
 if(!latestViewElement) throw new Error("No view element yet")
 const contents = rendition.getContents()[0];
 if(!contents) throw new Error("Why no contents??");
 const range = contents.range(cfiRange);
 if(!range) throw new Error("Input range is not in current view");
 const bounds = range.getBoundingClientRect();
 const viewElementBounds = latestViewElement.getBoundingClientRect();

 return {
     top: Math.abs(bounds.top) - Math.abs(viewElementBounds.top),
     left: Math.abs(bounds.left) - Math.abs(viewElementBounds.left) 
 };

}

Not sure if it will fix your exact issue, as this is only tested on a paginated 2 column layout. Hope it helps!

johnfactotum commented 5 years ago

Instead of keeping a latestViewElement, which doesn't work sometimes with continuous scrolled layout, getting the position of the frame seems to work well:

const section = book.spine.get(cfi)
const frameBounds = section.document.defaultView.frameElement.getBoundingClientRect()
// top: bounds.top + frameBounds.top
// etc.
Alice0h commented 5 years ago

@johnfactotum I'm wondering how you are getting defaultView populated from the section document? When I follow your code I get null as the defaultView. I'm trying to get the position in a continuous scrolled layout.

johnfactotum commented 5 years ago

I not sure but I think one needs to use rendition.getContents() and obtain the contents object (similar to @dmisdm's code above), then use contents.document.defaultView.frameElement. In continuous scrolled layout I think you will often get more than one contents when two or more pages are displayed at the same time, and you need to figure out which one is the one you need.

In my own code I use it in a hook like this, and the hook will provide you with the right content object:

rendition.hooks.content.register((contents, view) => {
    const frame = contents.document.defaultView.frameElement
    const viewElementRect = frame.getBoundingClientRect()

    // `target` can be an element or a range
    const rect = target.getBoundingClientRect()

    const left = rect.left + viewElementRect.left
    // etc.
}