diff --git a/Example/Podfile b/Example/Podfile index 9e61cf01a..414a27ad3 100755 --- a/Example/Podfile +++ b/Example/Podfile @@ -1,4 +1,4 @@ -platform :ios, '9.0' +platform :ios, '10.0' use_frameworks! inhibit_all_warnings! diff --git a/Example/Podfile.lock b/Example/Podfile.lock index a04b62021..e05cb3751 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -25,7 +25,7 @@ DEPENDENCIES: - Quick (= 1.3.2) SPEC REPOS: - https://github.com/cocoapods/specs.git: + https://github.com/CocoaPods/Specs.git: - AEXML - FontBlaster - MenuItemKit diff --git a/FolioReaderKit.podspec b/FolioReaderKit.podspec index a056d5076..ecb2c5fc0 100755 --- a/FolioReaderKit.podspec +++ b/FolioReaderKit.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.social_media_url = 'https://twitter.com/hebertialmeida' s.swift_version = '4.2' - s.platform = :ios, '9.0' + s.platform = :ios, '10.0' s.requires_arc = true s.source_files = [ diff --git a/Source/FolioReaderAudioPlayer.swift b/Source/FolioReaderAudioPlayer.swift index e2cd6158a..0fdf67cf5 100644 --- a/Source/FolioReaderAudioPlayer.swift +++ b/Source/FolioReaderAudioPlayer.swift @@ -382,22 +382,19 @@ open class FolioReaderAudioPlayer: NSObject { } let playbackActiveClass = book.playbackActiveClass - guard let sentence = currentPage.webView?.js("getSentenceWithIndex('\(playbackActiveClass)')") else { - if (readerCenter.isLastPage() == true) { - self.stop() - } else { - readerCenter.changePageToNext() + currentPage.webView?.js("getSentenceWithIndex('\(playbackActiveClass)')") { sentence in + guard let sentence = sentence else { + if (readerCenter.isLastPage() == true) { + self.stop() + } else { + readerCenter.changePageToNext() + } + return } - - return + guard let href = readerCenter.getCurrentChapter()?.href else { return } + // TODO QUESTION: The previous code made it possible to call `playText` with the parameter `href` being an empty string. Was that valid? should this logic be kept? + self.playText(href, text: sentence) } - - guard let href = readerCenter.getCurrentChapter()?.href else { - return - } - - // TODO QUESTION: The previous code made it possible to call `playText` with the parameter `href` being an empty string. Was that valid? should this logic be kept? - self.playText(href, text: sentence) } func readCurrentSentence() { diff --git a/Source/FolioReaderCenter.swift b/Source/FolioReaderCenter.swift index 7042ea769..5ec84230e 100644 --- a/Source/FolioReaderCenter.swift +++ b/Source/FolioReaderCenter.swift @@ -8,6 +8,7 @@ import UIKit import ZFDragableModalTransition +import WebKit /// Protocol which is used from `FolioReaderCenter`s. @objc public protocol FolioReaderCenterDelegate: class { @@ -464,17 +465,11 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl guard var html = try? String(contentsOfFile: resource.fullHref, encoding: String.Encoding.utf8) else { return cell } + + // Inject viewport + let viewportTag = "" - let mediaOverlayStyleColors = "\"\(self.readerConfig.mediaOverlayColor.hexString(false))\", \"\(self.readerConfig.mediaOverlayColor.highlightColor().hexString(false))\"" - - // Inject CSS - let jsFilePath = Bundle.frameworkBundle().path(forResource: "Bridge", ofType: "js") - let cssFilePath = Bundle.frameworkBundle().path(forResource: "Style", ofType: "css") - let cssTag = "" - let jsTag = "" + - "" - - let toInject = "\n\(cssTag)\n\(jsTag)\n" + let toInject = "\n\(viewportTag)\n" html = html.replacingOccurrences(of: "", with: toInject) // Font class name @@ -648,18 +643,13 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl } scrollScrubber?.setSliderVal() - - if let readingTime = currentPage.webView?.js("getReadingTime()") { - pageIndicatorView?.totalMinutes = Int(readingTime)! - } else { - pageIndicatorView?.totalMinutes = 0 + currentPage.webView?.js("getReadingTime()") { readingTime in + self.pageIndicatorView?.totalMinutes = Int(readingTime ?? "0")! + self.pagesForCurrentPage(currentPage) + self.delegate?.pageDidAppear?(currentPage) + self.delegate?.pageItemChanged?(self.getCurrentPageItemNumber()) + completion?() } - pagesForCurrentPage(currentPage) - - delegate?.pageDidAppear?(currentPage) - delegate?.pageItemChanged?(self.getCurrentPageItemNumber()) - - completion?() } func pagesForCurrentPage(_ page: FolioReaderPage?) { @@ -1075,9 +1065,10 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl @objc func shareChapter(_ sender: UIBarButtonItem) { guard let currentPage = currentPage else { return } - if let chapterText = currentPage.webView?.js("getBodyText()") { + currentPage.webView?.js("getBodyText()") { chapterText in + guard let chapterText = chapterText else { return } let htmlText = chapterText.replacingOccurrences(of: "[\\n\\r]+", with: "
", options: .regularExpression) - var subject = readerConfig.localizedShareChapterSubject + var subject = self.readerConfig.localizedShareChapterSubject var html = "" var text = "" var bookTitle = "" @@ -1092,7 +1083,7 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl } // Get chapter name - if let chapter = getCurrentChapterName() { + if let chapter = self.getCurrentChapterName() { chapterName = chapter } @@ -1104,17 +1095,17 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl // Sharing html and text html = "" html += "

\(htmlText)



" - html += "

"+readerConfig.localizedShareAllExcerptsFrom+"

" + html += "

"+self.readerConfig.localizedShareAllExcerptsFrom+"

" html += "\(bookTitle)
" - html += readerConfig.localizedShareBy+" \(authorName)
" - - if let bookShareLink = readerConfig.localizedShareWebLink { + html += self.readerConfig.localizedShareBy+" \(authorName)
" + + if let bookShareLink = self.readerConfig.localizedShareWebLink { html += "\(bookShareLink.absoluteString)" shareItems.append(bookShareLink as AnyObject) } html += "
" - text = "\(chapterName)\n\n“\(chapterText)” \n\n\(bookTitle) \n\(readerConfig.localizedShareBy) \(authorName)" + text = "\(chapterName)\n\n“\(chapterText)” \n\n\(bookTitle) \n\(self.readerConfig.localizedShareBy) \(authorName)" let act = FolioReaderSharingProvider(subject: subject, text: text, html: html) shareItems.insert(contentsOf: [act, "" as AnyObject], at: 0) @@ -1127,7 +1118,7 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl actv.barButtonItem = sender } - present(activityViewController, animated: true, completion: nil) + self.present(activityViewController, animated: true, completion: nil) } } @@ -1281,7 +1272,7 @@ open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UIColl // Perform the page after a short delay as the collection view hasn't completed it's transition if this method is called (the index paths aren't right during fast scrolls). delay(0.2, closure: { [weak self] in if (self?.readerConfig.scrollDirection == .horizontalWithVerticalContent), - let cell = ((scrollView.superview as? UIWebView)?.delegate as? FolioReaderPage) { + let cell = ((scrollView.superview as? WKWebView)?.navigationDelegate as? FolioReaderPage) { let currentIndexPathRow = cell.pageNumber - 1 self?.currentWebViewScrollPositions[currentIndexPathRow] = scrollView.contentOffset } diff --git a/Source/FolioReaderPage.swift b/Source/FolioReaderPage.swift index 5b013a0fd..8ee1e3521 100755 --- a/Source/FolioReaderPage.swift +++ b/Source/FolioReaderPage.swift @@ -9,6 +9,7 @@ import UIKit import SafariServices import MenuItemKit +import WebKit /// Protocol which is used from `FolioReaderPage`s. @objc public protocol FolioReaderPageDelegate: class { @@ -35,7 +36,7 @@ import MenuItemKit @objc optional func pageTap(_ recognizer: UITapGestureRecognizer) } -open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRecognizerDelegate { +open class FolioReaderPage: UICollectionViewCell, WKNavigationDelegate, UIGestureRecognizerDelegate { weak var delegate: FolioReaderPageDelegate? weak var readerContainer: FolioReaderContainer? @@ -80,13 +81,12 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe if webView == nil { webView = FolioReaderWebView(frame: webViewFrame(), readerContainer: readerContainer) webView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] - webView?.dataDetectorTypes = .link webView?.scrollView.showsVerticalScrollIndicator = false webView?.scrollView.showsHorizontalScrollIndicator = false webView?.backgroundColor = .clear self.contentView.addSubview(webView!) } - webView?.delegate = self + webView?.navigationDelegate = self if colorView == nil { colorView = UIView() @@ -110,7 +110,7 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe deinit { webView?.scrollView.delegate = nil - webView?.delegate = nil + webView?.navigationDelegate = nil NotificationCenter.default.removeObserver(self) } @@ -187,14 +187,20 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe return tempHtmlContent as String } - // MARK: - UIWebView Delegate + // MARK: - WKNavigation Delegate - open func webViewDidFinishLoad(_ webView: UIWebView) { + open func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { guard let webView = webView as? FolioReaderWebView else { return } delegate?.pageWillLoad?(self) + } + + open func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + guard let webView = webView as? FolioReaderWebView else { + return + } // Add the custom class based onClick listener self.setupClassBasedOnClickListeners() @@ -221,11 +227,22 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe webView.isColors = false self.webView?.createMenu(options: false) }) + webView.js("document.readyState") { _ in + self.delegate?.pageDidLoad?(self) + } + let overlayColor = readerConfig.mediaOverlayColor! + let colors = "\"\(overlayColor.hexString(false))\", \"\(overlayColor.highlightColor().hexString(false))\"" + webView.js("setMediaOverlayStyleColors(\(colors))") + } - delegate?.pageDidLoad?(self) + open func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + let handledAction = handlePolicy(for: navigationAction) + let policy: WKNavigationActionPolicy = handledAction ? .allow : .cancel + decisionHandler(policy) } - open func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebView.NavigationType) -> Bool { + private func handlePolicy(for navigationAction: WKNavigationAction) -> Bool { + let request = navigationAction.request guard let webView = webView as? FolioReaderWebView, let scheme = request.url?.scheme else { @@ -299,7 +316,7 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe } else if scheme == "mailto" { print("Email") return true - } else if url.absoluteString != "about:blank" && scheme.contains("http") && navigationType == .linkClicked { + } else if url.absoluteString != "about:blank" && scheme.contains("http") && navigationAction.navigationType == .linkActivated { let safariVC = SFSafariViewController(url: request.url!) safariVC.view.tintColor = self.readerConfig.tintColor self.folioReader.readerCenter?.present(safariVC, animated: true, completion: nil) @@ -373,20 +390,20 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe self.delegate?.pageTap?(recognizer) if let _navigationController = self.folioReader.readerCenter?.navigationController, (_navigationController.isNavigationBarHidden == true) { - let selected = webView?.js("getSelectedText()") + webView?.js("getSelectedText()") { selected in + guard (selected == nil || selected?.isEmpty == true) else { + return + } - guard (selected == nil || selected?.isEmpty == true) else { - return + let delay = 0.4 * Double(NSEC_PER_SEC) // 0.4 seconds * nanoseconds per seconds + let dispatchTime = (DispatchTime.now() + (Double(Int64(delay)) / Double(NSEC_PER_SEC))) + + DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: { + if (self.shouldShowBar == true && self.menuIsVisible == false) { + self.folioReader.readerCenter?.toggleBars() + } + }) } - - let delay = 0.4 * Double(NSEC_PER_SEC) // 0.4 seconds * nanoseconds per seconds - let dispatchTime = (DispatchTime.now() + (Double(Int64(delay)) / Double(NSEC_PER_SEC))) - - DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: { - if (self.shouldShowBar == true && self.menuIsVisible == false) { - self.folioReader.readerCenter?.toggleBars() - } - }) } else if (self.readerConfig.shouldHideNavigationOnTap == true) { self.folioReader.readerCenter?.hideBars() self.menuIsVisible = false @@ -433,19 +450,19 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe */ open func handleAnchor(_ anchor: String, avoidBeginningAnchors: Bool, animated: Bool) { if !anchor.isEmpty { - let offset = getAnchorOffset(anchor) - - switch self.readerConfig.scrollDirection { - case .vertical, .defaultVertical: - let isBeginning = (offset < frame.forDirection(withConfiguration: self.readerConfig) * 0.5) - - if !avoidBeginningAnchors { - scrollPageToOffset(offset, animated: animated) - } else if avoidBeginningAnchors && !isBeginning { - scrollPageToOffset(offset, animated: animated) + getAnchorOffset(anchor) { offset in + switch self.readerConfig.scrollDirection { + case .vertical, .defaultVertical: + let isBeginning = (offset < self.frame.forDirection(withConfiguration: self.readerConfig) * 0.5) + + if !avoidBeginningAnchors { + self.scrollPageToOffset(offset, animated: animated) + } else if avoidBeginningAnchors && !isBeginning { + self.scrollPageToOffset(offset, animated: animated) + } + case .horizontal, .horizontalWithVerticalContent: + self.scrollPageToOffset(offset, animated: animated) } - case .horizontal, .horizontalWithVerticalContent: - scrollPageToOffset(offset, animated: animated) } } } @@ -458,13 +475,15 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe - parameter anchor: The #anchor id - returns: The element offset ready to scroll */ - func getAnchorOffset(_ anchor: String) -> CGFloat { + func getAnchorOffset(_ anchor: String, completion: @escaping ((CGFloat) -> ())) { let horizontal = self.readerConfig.scrollDirection == .horizontal - if let strOffset = webView?.js("getAnchorOffset('\(anchor)', \(horizontal.description))") { - return CGFloat((strOffset as NSString).floatValue) + webView?.js("getAnchorOffset('\(anchor)', \(horizontal.description))") { strOffset in + guard let strOffset = strOffset else { + completion(CGFloat(0)) + return + } + completion(CGFloat((strOffset as NSString).floatValue)) } - - return CGFloat(0) } // MARK: Mark ID @@ -494,11 +513,13 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe } if !webView.isShare && !webView.isColors { - if let result = webView.js("getSelectedText()") , result.components(separatedBy: " ").count == 1 { + webView.js("getSelectedText()") { result in + guard let result = result, result.components(separatedBy: " ").count == 1 else { + webView.isOneWord = false + return + } webView.isOneWord = true webView.createMenu(options: false) - } else { - webView.isOneWord = false } } @@ -511,11 +532,12 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe if (self.folioReader.nightMode == true) { // omit create webView and colorView - let script = "document.documentElement.offsetHeight" - let contentHeight = webView.stringByEvaluatingJavaScript(from: script) - let frameHeight = webView.frame.height - let lastPageHeight = frameHeight * CGFloat(webView.pageCount) - CGFloat(Double(contentHeight!)!) - colorView.frame = CGRect(x: webView.frame.width * CGFloat(webView.pageCount-1), y: webView.frame.height - lastPageHeight, width: webView.frame.width, height: lastPageHeight) + // let script = "document.documentElement.offsetHeight" + // let contentHeight = webView.stringByEvaluatingJavaScript(from: script) + // let frameHeight = webView.frame.height + // let lastPageHeight = frameHeight * CGFloat(webView.pageCount) - CGFloat(Double(contentHeight!)!) + // colorView.frame = CGRect(x: webView.frame.width * CGFloat(webView.pageCount-1), y: webView.frame.height - lastPageHeight, width: webView.frame.width, height: lastPageHeight) + colorView.frame = CGRect.zero } else { colorView.frame = CGRect.zero } @@ -525,18 +547,8 @@ open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRe fileprivate func setupClassBasedOnClickListeners() { for listener in self.readerConfig.classBasedOnClickListeners { - self.webView?.js("addClassBasedOnClickListener(\"\(listener.schemeName)\", \"\(listener.querySelector)\", \"\(listener.attributeName)\", \"\(listener.selectAll)\")"); + self.webView?.js("addClassBasedOnClickListener(\"\(listener.schemeName)\", \"\(listener.querySelector)\", \"\(listener.attributeName)\", \"\(listener.selectAll)\")") } } - // MARK: - Public Java Script injection - - /** - Runs a JavaScript script and returns it result. The result of running the JavaScript script passed in the script parameter, or nil if the script fails. - - - returns: The result of running the JavaScript script passed in the script parameter, or nil if the script fails. - */ - open func performJavaScript(_ javaScriptCode: String) -> String? { - return webView?.js(javaScriptCode) - } } diff --git a/Source/FolioReaderPageIndicator.swift b/Source/FolioReaderPageIndicator.swift index fc1960556..dfa56052c 100644 --- a/Source/FolioReaderPageIndicator.swift +++ b/Source/FolioReaderPageIndicator.swift @@ -95,7 +95,12 @@ class FolioReaderPageIndicator: UIView { pagesLabel.text = " \(pagesRemaining) " + self.readerConfig.localizedReaderManyPagesLeft } - let minutesRemaining = Int(ceil(CGFloat((pagesRemaining * totalMinutes)/totalPages))) + let minutesRemaining: Int + if totalPages == 0 { + minutesRemaining = 0 + } else { + minutesRemaining = Int(ceil(CGFloat((pagesRemaining * totalMinutes)/totalPages))) + } if minutesRemaining > 1 { minutesLabel.text = "\(minutesRemaining) " + self.readerConfig.localizedReaderManyMinutes+" ·" } else if minutesRemaining == 1 { diff --git a/Source/FolioReaderScript.swift b/Source/FolioReaderScript.swift new file mode 100644 index 000000000..1f0140df4 --- /dev/null +++ b/Source/FolioReaderScript.swift @@ -0,0 +1,62 @@ +// +// FolioReaderScript.swift +// FolioReaderKit +// +// Created by Stanislav on 12.06.2020. +// Copyright (c) 2015 Folio Reader. All rights reserved. +// + +import WebKit + +class FolioReaderScript: WKUserScript { + + init(source: String) { + super.init(source: source, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true) + } + + static let bridgeJS: FolioReaderScript = { + let jsURL = Bundle.frameworkBundle().url(forResource: "Bridge", withExtension: "js")! + let jsSource = try! String(contentsOf: jsURL) + return FolioReaderScript(source: jsSource) + }() + + static let cssInjection: FolioReaderScript = { + let cssURL = Bundle.frameworkBundle().url(forResource: "Style", withExtension: "css")! + let cssString = try! String(contentsOf: cssURL) + return FolioReaderScript(source: cssInjectionSource(for: cssString)) + }() + + static func cssInjection(overflow: String) -> FolioReaderScript { + let cssString = "html{overflow:\(overflow)}" + return FolioReaderScript(source: cssInjectionSource(for: cssString)) + } + + private static func cssInjectionSource(for content: String) -> String { + let oneLineContent = content.components(separatedBy: .newlines).joined() + let source = """ + var style = document.createElement('style'); + style.type = 'text/css' + style.innerHTML = '\(oneLineContent)'; + document.head.appendChild(style); + """ + return source + } + +} + +extension WKUserScript { + + func addIfNeeded(to webView: WKWebView?) { + guard let controller = webView?.configuration.userContentController else { return } + let alreadyAdded = controller.userScripts.contains { [unowned self] in + return $0.source == self.source && + $0.injectionTime == self.injectionTime && + $0.isForMainFrameOnly == self.isForMainFrameOnly + } + if alreadyAdded { return } + controller.addUserScript(self) + } + +} diff --git a/Source/FolioReaderWebView.swift b/Source/FolioReaderWebView.swift index 9c3b7531a..578ddb340 100644 --- a/Source/FolioReaderWebView.swift +++ b/Source/FolioReaderWebView.swift @@ -6,13 +6,21 @@ // Copyright (c) 2016 Folio Reader. All rights reserved. // -import UIKit +import WebKit + +public typealias JSCallback = (String?) ->() /// The custom WebView used in each page -open class FolioReaderWebView: UIWebView { +open class FolioReaderWebView: WKWebView { var isColors = false var isShare = false var isOneWord = false + + fileprivate(set) var cssOverflowProperty = "scroll" { + didSet { + FolioReaderScript.cssInjection(overflow: cssOverflowProperty).addIfNeeded(to: self) + } + } fileprivate weak var readerContainer: FolioReaderContainer? @@ -31,14 +39,14 @@ open class FolioReaderWebView: UIWebView { return readerContainer.folioReader } - override init(frame: CGRect) { - fatalError("use init(frame:readerConfig:book:) instead.") - } - init(frame: CGRect, readerContainer: FolioReaderContainer) { self.readerContainer = readerContainer - super.init(frame: frame) + let configuration = WKWebViewConfiguration() + configuration.dataDetectorTypes = .link + super.init(frame: frame, configuration: configuration) + FolioReaderScript.cssInjection.addIfNeeded(to: self) + FolioReaderScript.bridgeJS.addIfNeeded(to: self) } required public init?(coder aDecoder: NSCoder) { @@ -77,11 +85,13 @@ open class FolioReaderWebView: UIWebView { let shareImage = UIAlertAction(title: self.readerConfig.localizedShareImageQuote, style: .default, handler: { (action) -> Void in if self.isShare { - if let textToShare = self.js("getHighlightContent()") { + self.js("getHighlightContent()") { textToShare in + guard let textToShare = textToShare else { return } self.folioReader.readerCenter?.presentQuoteShare(textToShare) } } else { - if let textToShare = self.js("getSelectedText()") { + self.js("getSelectedText()") { textToShare in + guard let textToShare = textToShare else { return } self.folioReader.readerCenter?.presentQuoteShare(textToShare) self.clearTextSelection() @@ -92,11 +102,13 @@ open class FolioReaderWebView: UIWebView { let shareText = UIAlertAction(title: self.readerConfig.localizedShareTextQuote, style: .default) { (action) -> Void in if self.isShare { - if let textToShare = self.js("getHighlightContent()") { + self.js("getHighlightContent()") { textToShare in + guard let textToShare = textToShare else { return } self.folioReader.readerCenter?.shareHighlight(textToShare, rect: sender.menuFrame) } } else { - if let textToShare = self.js("getSelectedText()") { + self.js("getSelectedText()") { textToShare in + guard let textToShare = textToShare else { return } self.folioReader.readerCenter?.shareHighlight(textToShare, rect: sender.menuFrame) } } @@ -124,92 +136,103 @@ open class FolioReaderWebView: UIWebView { } func remove(_ sender: UIMenuController?) { - if let removedId = js("removeThisHighlight()") { + js("removeThisHighlight()") { removedId in + guard let removedId = removedId else { return } Highlight.removeById(withConfiguration: self.readerConfig, highlightId: removedId) } setMenuVisible(false) } @objc func highlight(_ sender: UIMenuController?) { - let highlightAndReturn = js("highlightString('\(HighlightStyle.classForStyle(self.folioReader.currentHighlightStyle))')") - let jsonData = highlightAndReturn?.data(using: String.Encoding.utf8) - - do { - let json = try JSONSerialization.jsonObject(with: jsonData!, options: []) as! NSArray - let dic = json.firstObject as! [String: String] - let rect = NSCoder.cgRect(for: dic["rect"]!) - guard let startOffset = dic["startOffset"] else { - return - } - guard let endOffset = dic["endOffset"] else { - return - } - - createMenu(options: true) - setMenuVisible(true, andRect: rect) - - // Persist - guard - let html = js("getHTML()"), - let identifier = dic["id"], - let bookId = (self.book.name as NSString?)?.deletingPathExtension else { + js("highlightString('\(HighlightStyle.classForStyle(self.folioReader.currentHighlightStyle))')") { highlightAndReturn in + let jsonData = highlightAndReturn?.data(using: String.Encoding.utf8) + + do { + let json = try JSONSerialization.jsonObject(with: jsonData!, options: []) as! NSArray + let dic = json.firstObject as! [String: String] + let rect = NSCoder.cgRect(for: dic["rect"]!) + guard let startOffset = dic["startOffset"] else { return - } - - let pageNumber = folioReader.readerCenter?.currentPageNumber ?? 0 - let match = Highlight.MatchingHighlight(text: html, id: identifier, startOffset: startOffset, endOffset: endOffset, bookId: bookId, currentPage: pageNumber) - let highlight = Highlight.matchHighlight(match) - highlight?.persist(withConfiguration: self.readerConfig) + } + guard let endOffset = dic["endOffset"] else { + return + } - } catch { - print("Could not receive JSON") + self.createMenu(options: true) + self.setMenuVisible(true, andRect: rect) + + // Persist + self.js("getHTML()") { html in + + guard let html = html, let identifier = dic["id"], let bookId = (self.book.name as NSString?)?.deletingPathExtension + else { + return + } + let pageNumber = self.folioReader.readerCenter?.currentPageNumber ?? 0 + let match = Highlight.MatchingHighlight(text: html, id: identifier, startOffset: startOffset, endOffset: endOffset, bookId: bookId, currentPage: pageNumber) + let highlight = Highlight.matchHighlight(match) + highlight?.persist(withConfiguration: self.readerConfig) + } + + } catch { + print("Could not receive JSON:", error) + } } } @objc func highlightWithNote(_ sender: UIMenuController?) { - let highlightAndReturn = js("highlightStringWithNote('\(HighlightStyle.classForStyle(self.folioReader.currentHighlightStyle))')") - let jsonData = highlightAndReturn?.data(using: String.Encoding.utf8) + js("highlightStringWithNote('\(HighlightStyle.classForStyle(self.folioReader.currentHighlightStyle))')") { highlightAndReturn in + let jsonData = highlightAndReturn?.data(using: String.Encoding.utf8) - do { - let json = try JSONSerialization.jsonObject(with: jsonData!, options: []) as! NSArray - let dic = json.firstObject as! [String: String] - guard let startOffset = dic["startOffset"] else { return } - guard let endOffset = dic["endOffset"] else { return } - - self.clearTextSelection() + do { + let json = try JSONSerialization.jsonObject(with: jsonData!, options: []) as! NSArray + let dic = json.firstObject as! [String: String] + guard let startOffset = dic["startOffset"] else { return } + guard let endOffset = dic["endOffset"] else { return } - guard let html = js("getHTML()") else { return } - guard let identifier = dic["id"] else { return } - guard let bookId = (self.book.name as NSString?)?.deletingPathExtension else { return } + self.clearTextSelection() - let pageNumber = folioReader.readerCenter?.currentPageNumber ?? 0 - let match = Highlight.MatchingHighlight(text: html, id: identifier, startOffset: startOffset, endOffset: endOffset, bookId: bookId, currentPage: pageNumber) - if let highlight = Highlight.matchHighlight(match) { - self.folioReader.readerCenter?.presentAddHighlightNote(highlight, edit: false) + self.js("getHTML()") { html in + guard + let html = html, let identifier = dic["id"], + let bookId = (self.book.name as NSString?)?.deletingPathExtension else + { + return + } + + let pageNumber = self.folioReader.readerCenter?.currentPageNumber ?? 0 + let match = Highlight.MatchingHighlight(text: html, id: identifier, startOffset: startOffset, endOffset: endOffset, bookId: bookId, currentPage: pageNumber) + if let highlight = Highlight.matchHighlight(match) { + self.folioReader.readerCenter?.presentAddHighlightNote(highlight, edit: false) + } + } + } catch { + print("Could not receive JSON:", error) } - } catch { - print("Could not receive JSON") } } @objc func updateHighlightNote (_ sender: UIMenuController?) { - guard let highlightId = js("getHighlightId()") else { return } - guard let highlightNote = Highlight.getById(withConfiguration: readerConfig, highlightId: highlightId) else { return } - self.folioReader.readerCenter?.presentAddHighlightNote(highlightNote, edit: true) + js("getHighlightId()") { highlightId in + guard + let highlightId = highlightId, + let highlightNote = Highlight.getById(withConfiguration: self.readerConfig, highlightId: highlightId) else { return } + self.folioReader.readerCenter?.presentAddHighlightNote(highlightNote, edit: true) + } } @objc func define(_ sender: UIMenuController?) { - guard let selectedText = js("getSelectedText()") else { - return - } + js("getSelectedText()") { selectedText in + guard let selectedText = selectedText else { return } - self.setMenuVisible(false) - self.clearTextSelection() + self.setMenuVisible(false) + self.clearTextSelection() - let vc = UIReferenceLibraryViewController(term: selectedText) - vc.view.tintColor = self.readerConfig.tintColor - guard let readerContainer = readerContainer else { return } - readerContainer.show(vc, sender: nil) + let vc = UIReferenceLibraryViewController(term: selectedText) + vc.view.tintColor = self.readerConfig.tintColor + guard let readerContainer = self.readerContainer else { return } + readerContainer.show(vc, sender: nil) + } } @objc func play(_ sender: UIMenuController?) { @@ -241,7 +264,8 @@ open class FolioReaderWebView: UIWebView { func changeHighlightStyle(_ sender: UIMenuController?, style: HighlightStyle) { self.folioReader.currentHighlightStyle = style.rawValue - if let updateId = js("setHighlightStyle('\(HighlightStyle.classForStyle(style.rawValue))')") { + js("setHighlightStyle('\(HighlightStyle.classForStyle(style.rawValue))')") { updateId in + guard let updateId = updateId else { return } Highlight.updateById(withConfiguration: self.readerConfig, highlightId: updateId, type: style) } @@ -346,10 +370,29 @@ open class FolioReaderWebView: UIWebView { // MARK: - Java Script Bridge - @discardableResult open func js(_ script: String) -> String? { - let callback = self.stringByEvaluatingJavaScript(from: script) - if callback!.isEmpty { return nil } - return callback + open func js(_ script: String, completion: JSCallback? = nil) { + evaluateJavaScript(script) { result, error in + let output: String? + if let result = result { + let stringResult = "\(result)" + if stringResult.isEmpty { + output = nil + } else { + output = stringResult + } + } else { + output = nil + } + if let nsError = error as? NSError, + let url = nsError.userInfo["WKJavaScriptExceptionSourceURL"] as? NSURL, + url.absoluteString == "undefined" + { + // skip debugPrint - html hasn't loaded yet + } else if let error = error { + debugPrint("evaluateJavaScript(\(script)) returned an error:", error) + } + completion?(output) + } } // MARK: WebView @@ -366,13 +409,12 @@ open class FolioReaderWebView: UIWebView { switch self.readerConfig.scrollDirection { case .vertical, .defaultVertical, .horizontalWithVerticalContent: scrollView.isPagingEnabled = false - paginationMode = .unpaginated + cssOverflowProperty = "scroll" scrollView.bounces = true break case .horizontal: scrollView.isPagingEnabled = true - paginationMode = .leftToRight - paginationBreakingMode = .page + cssOverflowProperty = "-webkit-paged-x" scrollView.bounces = false break } diff --git a/Source/Models/Highlight+Helper.swift b/Source/Models/Highlight+Helper.swift index 3d23a4054..537bd0fcf 100644 --- a/Source/Models/Highlight+Helper.swift +++ b/Source/Models/Highlight+Helper.swift @@ -288,16 +288,9 @@ extension Highlight { /// - Parameters: /// - page: The page containing the HTML. /// - highlightId: The ID to be removed - /// - Returns: The removed id - @discardableResult public static func removeFromHTMLById(withinPage page: FolioReaderPage?, highlightId: String) -> String? { - guard let currentPage = page else { return nil } - - if let removedId = currentPage.webView?.js("removeHighlightById('\(highlightId)')") { - return removedId - } else { - print("Error removing Highlight from page") - return nil - } + /// - completion: JSCallback with removed id + public static func removeFromHTMLById(withinPage page: FolioReaderPage?, highlightId: String, completion: JSCallback? = nil) { + page?.webView?.js("removeHighlightById('\(highlightId)')", completion: completion) } /**