Skip to content
This repository has been archived by the owner on Aug 12, 2022. It is now read-only.

Commit

Permalink
Merge pull request #118 from tooolbox/dev/feature/footnotes
Browse files Browse the repository at this point in the history
Pop-Up Footnotes
  • Loading branch information
mickael-menu authored Apr 28, 2020
2 parents 6654a5d + e4556f5 commit 8418854
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 26 deletions.
1 change: 1 addition & 0 deletions Cartfile
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
github "readium/r2-shared-swift" == 1.4.3
github "scinfu/SwiftSoup" == 2.3.1
1 change: 1 addition & 0 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
github "readium/r2-shared-swift" "1.4.3"
github "scinfu/SwiftSoup" "2.3.1"
7 changes: 3 additions & 4 deletions r2-navigator-swift/EPUB/EPUBFixedSpreadView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,9 @@ final class EPUBFixedSpreadView: EPUBSpreadView {
""")
}

override func pointFromTap(_ data: [String : Any]) -> CGPoint? {
guard let x = data["screenX"] as? Int, let y = data["screenY"] as? Int else {
return nil
}
override func pointFromTap(_ data: TapData) -> CGPoint? {
let x = data.screenX
let y = data.screenY

return CGPoint(
x: CGFloat(x) * scrollView.zoomScale - scrollView.contentOffset.x + webView.frame.minX,
Expand Down
73 changes: 72 additions & 1 deletion r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import UIKit
import R2Shared
import WebKit
import SafariServices
import SwiftSoup


public protocol EPUBNavigatorDelegate: VisualNavigatorDelegate {
Expand Down Expand Up @@ -366,10 +367,80 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate {
delegate?.navigator(self, presentExternalURL: url)
}

func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String) {
func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String, tapData: TapData?) {

// Check to see if this was a noteref link and give delegate the opportunity to display it.
if
let tapData = tapData,
let interactive = tapData.interactiveElement,
let (note, referrer) = getNoteData(anchor: interactive, href: href),
let delegate = self.delegate
{
if !delegate.navigator(
self,
shouldNavigateToNoteAt: Link(href: href, type: "text/html"),
content: note,
referrer: referrer
) {
return
}
}

go(to: Link(href: href))
}

/// Checks if the internal link is a noteref, and retrieves both the referring text of the link and the body of the note.
///
/// Uses the navigation href from didTapOnInternalLink because it is normalized to a path within the book,
/// whereas the anchor tag may have just a hash fragment like `#abc123` which is hard to work with.
/// We do at least validate to ensure that the two hrefs match.
///
/// Uses `#id` when retrieving the body of the note, not `aside#id` because it may be a `<section>`.
/// See https://idpf.github.io/epub-vocabs/structure/#footnotes
/// and http://kb.daisy.org/publishing/docs/html/epub-type.html#ex
func getNoteData(anchor: String, href: String) -> (String, String)? {
do {
let doc = try parse(anchor)
guard let link = try doc.select("a[epub:type=noteref]").first() else { return nil }

let anchorHref = try link.attr("href")
guard href.hasSuffix(anchorHref) else { return nil}

let hashParts = href.split(separator: "#")
guard hashParts.count == 2 else {
log(.error, "Could not find hash in link \(href)")
return nil
}
let id = String(hashParts[1])
var withoutFragment = String(hashParts[0])
if withoutFragment.hasPrefix("/") {
withoutFragment = String(withoutFragment.dropFirst())
}

guard let base = publication.baseURL else {
log(.error, "Couldn't get publication base URL")
return nil
}

let absolute = base.appendingPathComponent(withoutFragment)

log(.debug, "Fetching note contents from \(absolute.absoluteString)")
let contents = try String(contentsOf: absolute)
let document = try parse(contents)

guard let aside = try document.select("#\(id)").first() else {
log(.error, "Could not find the element '#\(id)' in document \(absolute)")
return nil
}

return (try aside.html(), try link.html())

} catch {
log(.error, "Caught error while getting note content: \(error)")
return nil
}
}

func spreadViewPagesDidChange(_ spreadView: EPUBSpreadView) {
if paginationView.currentView == spreadView {
notifyCurrentLocation()
Expand Down
7 changes: 3 additions & 4 deletions r2-navigator-swift/EPUB/EPUBReflowableSpreadView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,9 @@ final class EPUBReflowableSpreadView: EPUBSpreadView {
}
}

override func pointFromTap(_ data: [String : Any]) -> CGPoint? {
guard let x = data["clientX"] as? Int, let y = data["clientY"] as? Int else {
return nil
}
override func pointFromTap(_ data: TapData) -> CGPoint? {
let x = data.clientX
let y = data.clientY

var point = CGPoint(x: x, y: y)
if isScrollEnabled {
Expand Down
54 changes: 45 additions & 9 deletions r2-navigator-swift/EPUB/EPUBSpreadView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import WebKit
import R2Shared
import SwiftSoup


protocol EPUBSpreadViewDelegate: class {
Expand All @@ -27,7 +28,7 @@ protocol EPUBSpreadViewDelegate: class {
func spreadView(_ spreadView: EPUBSpreadView, didTapOnExternalURL url: URL)

/// Called when the user tapped on an internal link.
func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String)
func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String, tapData: TapData?)

/// Called when the pages visible in the spread changed.
func spreadViewPagesDidChange(_ spreadView: EPUBSpreadView)
Expand All @@ -50,6 +51,8 @@ class EPUBSpreadView: UIView, Loggable {
let readingProgression: ReadingProgression
let userSettings: UserSettings
let editingActions: EditingActionsController

private var lastTap: TapData? = nil

/// If YES, the content will be faded in once loaded.
let animatedLoad: Bool
Expand Down Expand Up @@ -193,18 +196,26 @@ class EPUBSpreadView: UIView, Loggable {
}

/// Called from the JS code when a tap is detected.
private func didTap(_ body: Any) {
guard let body = body as? [String: Any],
let point = pointFromTap(body) else
{
return
/// If the JS indicates the tap is being handled within the webview, don't take action,
/// just save the tap data for use by webView(_ webView:decidePolicyFor:decisionHandler:)
private func didTap(_ data: Any) {
let tapData = TapData(data: data)
lastTap = tapData

guard !tapData.defaultPrevented else { return }
if let interactive = tapData.interactiveElement {
let isNoteref = (try? parse(interactive).select("a[epub:type=noteref]").first()) == nil
if !isNoteref {
return
}
}


guard let point = pointFromTap(tapData) else { return }
delegate?.spreadView(self, didTapAt: point)
}

/// Converts the touch data returned by the JavaScript `tap` event into a point in the webview's coordinate space.
func pointFromTap(_ data: [String: Any]) -> CGPoint? {
func pointFromTap(_ data: TapData) -> CGPoint? {
// To override in subclasses.
return nil
}
Expand Down Expand Up @@ -423,7 +434,7 @@ extension EPUBSpreadView: WKNavigationDelegate {
// Check if url is internal or external
if let baseURL = publication.baseURL, url.host == baseURL.host {
let href = url.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/")
delegate?.spreadView(self, didTapOnInternalLink: href)
delegate?.spreadView(self, didTapOnInternalLink: href, tapData: self.lastTap)
} else {
delegate?.spreadView(self, didTapOnExternalURL: url)
}
Expand Down Expand Up @@ -504,3 +515,28 @@ private extension EPUBSpreadView {
}

}

/// Produced by gestures.js
struct TapData {
let defaultPrevented: Bool
let screenX: Int
let screenY: Int
let clientX: Int
let clientY: Int
let targetElement: String
let interactiveElement: String?

init(dict: [String: Any]) {
self.defaultPrevented = dict["defaultPrevented"] as? Bool ?? false
self.screenX = dict["screenX"] as? Int ?? 0
self.screenY = dict["screenY"] as? Int ?? 0
self.clientX = dict["clientX"] as? Int ?? 0
self.clientY = dict["clientY"] as? Int ?? 0
self.targetElement = dict["targetElement"] as? String ?? ""
self.interactiveElement = dict["interactiveElement"] as? String
}

init(data: Any) {
self.init(dict: data as? [String: Any] ?? [String: Any]())
}
}
19 changes: 11 additions & 8 deletions r2-navigator-swift/EPUB/Resources/Scripts/gestures.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@
});

function onClick(event) {
if (event.defaultPrevented || isInteractiveElement(event.target)) {
return;
}

if (!window.getSelection().isCollapsed) {
// There's an on-going selection, the tap will dismiss it so we don't forward it.
return;
}

// Send the tap data over the JS bridge even if it's been handled
// within the webview, so that it can be preserved and used
// by the WKNavigationDelegate if needed.
webkit.messageHandlers.tap.postMessage({
"defaultPrevented": event.defaultPrevented,
"screenX": event.screenX,
"screenY": event.screenY,
"clientX": event.clientX,
"clientY": event.clientY,
"targetElement": event.target.outerHTML,
"interactiveElement": nearestInteractiveElement(event.target),
});

// We don't want to disable the default WebView behavior as it breaks some features without bringing any value.
Expand All @@ -29,7 +32,7 @@
}

// See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling
function isInteractiveElement(element) {
function nearestInteractiveElement(element) {
var interactiveTags = [
'a',
'audio',
Expand All @@ -45,20 +48,20 @@
'video',
]
if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) {
return true;
return element.outerHTML;
}

// Checks whether the element is editable by the user.
if (element.hasAttribute('contenteditable') && element.getAttribute('contenteditable').toLowerCase() != 'false') {
return true;
return element.outerHTML;
}

// Checks parents recursively because the touch might be for example on an <em> inside a <a>.
if (element.parentElement) {
return isInteractiveElement(element.parentElement);
return nearestInteractiveElement(element.parentElement);
}

return false;
return null;
}

})();
11 changes: 11 additions & 0 deletions r2-navigator-swift/Navigator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ public protocol NavigatorDelegate: AnyObject {
/// Called when the user tapped an external URL. The default implementation opens the URL with the default browser.
func navigator(_ navigator: Navigator, presentExternalURL url: URL)

/// Called when the user taps on a link referring to a note.
///
/// Return `true` to navigate to the note, or `false` if you intend to present the
/// note yourself, using its `content`. `link.type` contains information about the
/// format of `content` and `referrer`, such as `text/html`.
func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String, referrer: String?) -> Bool

}


Expand All @@ -96,6 +103,10 @@ public extension NavigatorDelegate {
UIApplication.shared.openURL(url)
}
}

func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String, referrer: String?) -> Bool {
return true
}

}

Expand Down

0 comments on commit 8418854

Please sign in to comment.