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

Pop-Up Footnotes #118

Merged
merged 6 commits into from
Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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