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 2 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"
64 changes: 61 additions & 3 deletions r2-navigator-swift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
03C3CC68222DBD8600A01731 /* R2Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03C3CC67222DBD8600A01731 /* R2Shared.framework */; };
0D77748B244F978A00A5E857 /* R2Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D777488244F977E00A5E857 /* R2Shared.framework */; };
0D77748D244F97F200A5E857 /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D77747E244F970E00A5E857 /* SwiftSoup.framework */; };
CA0B3AC3222EE555006D9363 /* PDFNavigatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0B3AC2222EE555006D9363 /* PDFNavigatorViewController.swift */; };
CA1E4F4B240037E6009C4DE3 /* CompletionList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA1E4F4A240037E6009C4DE3 /* CompletionList.swift */; };
CA26EF7E22803FE90011653E /* VisualNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA26EF7D22803FE90011653E /* VisualNavigator.swift */; };
Expand Down Expand Up @@ -35,8 +36,29 @@
F3E7D42E1F4EE0FE00DF166D /* CBZNavigatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E7D42D1F4EE0FE00DF166D /* CBZNavigatorViewController.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
tooolbox marked this conversation as resolved.
Show resolved Hide resolved
0D777487244F977E00A5E857 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = F3E7D3F41F4EBE2100DF166D;
remoteInfo = "r2-shared-swift";
};
0D777489244F977E00A5E857 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = CA6161EA21FB257700D2CFE3;
remoteInfo = "r2-shared-swiftTests";
};
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
03C3CC67222DBD8600A01731 /* R2Shared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = R2Shared.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0D77747E244F970E00A5E857 /* SwiftSoup.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSoup.framework; path = "../r2-testapp-swift/Carthage/Build/iOS/SwiftSoup.framework"; sourceTree = "<group>"; };
0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = "r2-shared-swift.xcodeproj"; path = "../r2-shared-swift/r2-shared-swift.xcodeproj"; sourceTree = "<group>"; };
0D82BF0D244EAC62006FDB31 /* SwiftSoup.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSoup.framework; path = Carthage/Build/iOS/SwiftSoup.framework; sourceTree = "<group>"; };
0D82BF11244EAE04006FDB31 /* R2Shared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = R2Shared.framework; path = Carthage/Build/iOS/R2Shared.framework; sourceTree = "<group>"; };
CA0B3AC2222EE555006D9363 /* PDFNavigatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFNavigatorViewController.swift; sourceTree = "<group>"; };
CA1E4F4A240037E6009C4DE3 /* CompletionList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionList.swift; sourceTree = "<group>"; };
CA26EF7D22803FE90011653E /* VisualNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualNavigator.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -73,13 +95,23 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
03C3CC68222DBD8600A01731 /* R2Shared.framework in Frameworks */,
0D77748B244F978A00A5E857 /* R2Shared.framework in Frameworks */,
0D77748D244F97F200A5E857 /* SwiftSoup.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
0D777483244F977E00A5E857 /* Products */ = {
isa = PBXGroup;
children = (
0D777488244F977E00A5E857 /* R2Shared.framework */,
0D77748A244F977E00A5E857 /* r2-shared-swiftTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
CA0B3AC1222EE530006D9363 /* PDF */ = {
isa = PBXGroup;
children = (
Expand All @@ -97,7 +129,6 @@
CAF1E3F422DF23F400E807EA /* PaginationView.swift */,
CAC2A6D62292E4BA000AA2A7 /* WebView.swift */,
CAD178B522B3B553004E6812 /* R2NavigatorLocalizedString.swift */,
CA1E4F4A240037E6009C4DE3 /* CompletionList.swift */,
);
path = Toolkit;
sourceTree = "<group>";
Expand Down Expand Up @@ -191,6 +222,10 @@
F3E7D3E71F4DC40800DF166D /* Frameworks */ = {
isa = PBXGroup;
children = (
0D77747E244F970E00A5E857 /* SwiftSoup.framework */,
0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */,
0D82BF11244EAE04006FDB31 /* R2Shared.framework */,
0D82BF0D244EAC62006FDB31 /* SwiftSoup.framework */,
03C3CC67222DBD8600A01731 /* R2Shared.framework */,
);
name = Frameworks;
Expand Down Expand Up @@ -255,13 +290,36 @@
mainGroup = F3E7D3B91F4D83B000DF166D;
productRefGroup = F3E7D3C41F4D83B000DF166D /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = 0D777483244F977E00A5E857 /* Products */;
ProjectRef = 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */;
},
);
projectRoot = "";
targets = (
F3E7D3C21F4D83B000DF166D /* r2-navigator-swift */,
);
};
/* End PBXProject section */

/* Begin PBXReferenceProxy section */
0D777488244F977E00A5E857 /* R2Shared.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
path = R2Shared.framework;
remoteRef = 0D777487244F977E00A5E857 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
0D77748A244F977E00A5E857 /* r2-shared-swiftTests.xctest */ = {
isa = PBXReferenceProxy;
fileType = wrapper.cfbundle;
path = "r2-shared-swiftTests.xctest";
remoteRef = 0D777489244F977E00A5E857 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
/* End PBXReferenceProxy section */

/* Begin PBXResourcesBuildPhase section */
F3E7D3C11F4D83B000DF166D /* Resources */ = {
isa = PBXResourcesBuildPhase;
Expand Down
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
62 changes: 61 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,69 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate {
delegate?.navigator(self, presentExternalURL: url)
}

func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String) {
func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String, anchor: String?) {
tooolbox marked this conversation as resolved.
Show resolved Hide resolved

// Check to see if this was a noteref link and give delegate the opportunity to display it.
if let anchor = anchor, let note = getNoteContent(anchor: anchor), let delegate = self.delegate {
if delegate.navigator(self,
shouldNavigateToNoteAt: Link(href: href),
tooolbox marked this conversation as resolved.
Show resolved Hide resolved
content: note,
source: anchor) == false {
return
}
tooolbox marked this conversation as resolved.
Show resolved Hide resolved
}

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

func getNoteContent(anchor: String) -> String? {
do {
let doc = try parse(anchor)
guard let link = try doc.select("a[epub:type=noteref]").first() else { return nil }

let href = try link.attr("href")
guard let hashIndex = href.lastIndex(of: "#") else {
log(.error, "Could not find hash in link \(href)")
return nil
}
let id = String(href[href.index(hashIndex, offsetBy: 1)...])
let withoutFragment = String(href[..<hashIndex])
tooolbox marked this conversation as resolved.
Show resolved Hide resolved

guard var loc = self.currentLocation?.href else {
log(.error, "Couldn't get current location")
return nil
}
if loc.hasPrefix("/") {
loc = String(loc.dropFirst())
}

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

let resource = base.appendingPathComponent(loc)
guard let absolute = URL(string: withoutFragment, relativeTo: resource) else {
log(.error, "Could not get absolute URL from \(withoutFragment) relative to \(self.resourcesURL?.absoluteString ?? "(no self.resourcesURL)")")
return nil
}

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()

} 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
47 changes: 37 additions & 10 deletions r2-navigator-swift/EPUB/EPUBSpreadView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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, anchor: String?)

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

var lastTap: TapData? = nil
tooolbox marked this conversation as resolved.
Show resolved Hide resolved

/// If YES, the content will be faded in once loaded.
let animatedLoad: Bool
Expand Down Expand Up @@ -193,18 +195,20 @@ 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.shouldHandle else { 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 +427,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, anchor: self.lastTap?.anchor)
} else {
delegate?.spreadView(self, didTapOnExternalURL: url)
}
Expand Down Expand Up @@ -504,3 +508,26 @@ private extension EPUBSpreadView {
}

}

/// Produced by gestures.js
struct TapData {
let shouldHandle: Bool
let screenX: Int
let screenY: Int
let clientX: Int
let clientY: Int
let anchor: String?

init(dict: [String: Any]) {
self.shouldHandle = dict["shouldHandle"] 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.anchor = dict["anchor"] as? String
}

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

function onClick(event) {

// If the app should handle the tap.
// Examples of the app handling the tap would be
// navigating left/right, or show/hide the toolbar.
// If false, the tap is being handled within the webview,
// such as with a hyperlink or by an publication's JS handler.
let appShouldHandle = true;
tooolbox marked this conversation as resolved.
Show resolved Hide resolved

if (event.defaultPrevented || isInteractiveElement(event.target)) {
return;
appShouldHandle = false;
}

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({
"shouldHandle": appShouldHandle,
"screenX": event.screenX,
"screenY": event.screenY,
"clientX": event.clientX,
"clientY": event.clientY,
"anchor": getNearestAnchor(event.target),
});

// We don't want to disable the default WebView behavior as it breaks some features without bringing any value.
Expand Down Expand Up @@ -61,4 +74,18 @@
return false;
}

// Retrieves the markup of <a>...</a> if the tap was
// anywhere within such an element (i.e. even on an <em> tag within it).
// We return the markup rather than just a boolean as this could be more
// useful further up the line.
function getNearestAnchor(element) {
if (element.nodeName.toLowerCase() === 'a') {
return element.outerHTML;
}
if (element.parentElement) {
return getNearestAnchor(element.parentElement);
}
return null;
}

})();
7 changes: 7 additions & 0 deletions r2-navigator-swift/Navigator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ 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 noteref link.
func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String, source: String) -> Bool
tooolbox marked this conversation as resolved.
Show resolved Hide resolved

}


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

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

}

Expand Down