2 //  FolioReaderPage.swift
 
   5 //  Created by Heberti Almeida on 10/04/15.
 
   6 //  Copyright (c) 2015 Folio Reader. All rights reserved.
 
  12 import JSQWebViewController
 
  14 /// Protocol which is used from `FolioReaderPage`s.
 
  15 @objc public protocol FolioReaderPageDelegate: class {
 
  18      Notify that the page will be loaded. Note: The webview content itself is already loaded at this moment. But some java script operations like the adding of class based on click listeners will happen right after this method. If you want to perform custom java script before this happens this method is the right choice. If you want to modify the html content (and not run java script) you have to use `htmlContentForPage()` from the `FolioReaderCenterDelegate`.
 
  20      - parameter page: The loaded page
 
  22     @objc optional func pageWillLoad(_ page: FolioReaderPage)
 
  25      Notifies that page did load. A page load doesn't mean that this page is displayed right away, use `pageDidAppear` to get informed about the appearance of a page.
 
  27      - parameter page: The loaded page
 
  29     @objc optional func pageDidLoad(_ page: FolioReaderPage)
 
  32      Notifies that page receive tap gesture.
 
  34      - parameter recognizer: The tap recognizer
 
  36     @objc optional func pageTap(_ recognizer: UITapGestureRecognizer)
 
  39 open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRecognizerDelegate {
 
  40     weak var delegate: FolioReaderPageDelegate?
 
  41     weak var readerContainer: FolioReaderContainer?
 
  43     /// The index of the current page. Note: The index start at 1!
 
  44     open var pageNumber: Int!
 
  45     open var webView: FolioReaderWebView?
 
  47     fileprivate var colorView: UIView!
 
  48     fileprivate var shouldShowBar = true
 
  49     fileprivate var menuIsVisible = false
 
  50     fileprivate var isNextChapter = true
 
  51     fileprivate var anchorTapped = false
 
  52     private(set) var pageLoaded = false
 
  54     fileprivate var readerConfig: FolioReaderConfig {
 
  55         guard let readerContainer = readerContainer else { return FolioReaderConfig() }
 
  56         return readerContainer.readerConfig
 
  59     fileprivate var book: FRBook {
 
  60         guard let readerContainer = readerContainer else { return FRBook() }
 
  61         return readerContainer.book
 
  64     fileprivate var folioReader: FolioReader {
 
  65         guard let readerContainer = readerContainer else { return FolioReader() }
 
  66         return readerContainer.folioReader
 
  69     // MARK: - View life cicle
 
  71     public override init(frame: CGRect) {
 
  72         // Init explicit attributes with a default value. The `setup` function MUST be called to configure the current object with valid attributes.
 
  73         self.readerContainer = FolioReaderContainer(withConfig: FolioReaderConfig(), folioReader: FolioReader(), epubPath: "")
 
  74         super.init(frame: frame)
 
  75         self.backgroundColor = UIColor.clear
 
  77         NotificationCenter.default.addObserver(self, selector: #selector(refreshPageMode), name: NSNotification.Name(rawValue: "needRefreshPageMode"), object: nil)
 
  80     public func setup(withReaderContainer readerContainer: FolioReaderContainer) {
 
  81         self.readerContainer = readerContainer
 
  82         guard let readerContainer = self.readerContainer else { return }
 
  85             webView = FolioReaderWebView(frame: webViewFrame(), readerContainer: readerContainer)
 
  86             webView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
 
  87             webView?.dataDetectorTypes = .link
 
  88             webView?.scrollView.showsVerticalScrollIndicator = false
 
  89             webView?.scrollView.showsHorizontalScrollIndicator = false
 
  90             webView?.backgroundColor = .clear
 
  91             self.contentView.addSubview(webView!)
 
  93         webView?.delegate = self
 
  97             colorView.backgroundColor = self.readerConfig.nightModeBackground
 
  98             webView?.scrollView.addSubview(colorView)
 
 101         // Remove all gestures before adding new one
 
 102         webView?.gestureRecognizers?.forEach({ gesture in
 
 103             webView?.removeGestureRecognizer(gesture)
 
 105         let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
 
 106         tapGestureRecognizer.numberOfTapsRequired = 1
 
 107         tapGestureRecognizer.delegate = self
 
 108         webView?.addGestureRecognizer(tapGestureRecognizer)
 
 111     required public init?(coder aDecoder: NSCoder) {
 
 112         fatalError("storyboards are incompatible with truth and beauty")
 
 116         webView?.scrollView.delegate = nil
 
 117         webView?.delegate = nil
 
 118         NotificationCenter.default.removeObserver(self)
 
 121     override open func layoutSubviews() {
 
 122         super.layoutSubviews()
 
 124         webView?.setupScrollDirection()
 
 125         webView?.frame = webViewFrame()
 
 128     func webViewFrame() -> CGRect {
 
 129         guard (self.readerConfig.hideBars == false) else {
 
 133         let statusbarHeight = UIApplication.shared.statusBarFrame.size.height
 
 134         let navBarHeight = self.folioReader.readerCenter?.navigationController?.navigationBar.frame.size.height ?? CGFloat(0)
 
 135         let navTotal = self.readerConfig.shouldHideNavigationOnTap ? 0 : statusbarHeight + navBarHeight
 
 136         let paddingTop: CGFloat = 20
 
 137         let paddingBottom: CGFloat = 30
 
 141             y: self.readerConfig.isDirection(bounds.origin.y + navTotal, bounds.origin.y + navTotal + paddingTop, bounds.origin.y + navTotal),
 
 143             height: self.readerConfig.isDirection(bounds.height - navTotal, bounds.height - navTotal - paddingTop - paddingBottom, bounds.height - navTotal)
 
 147     func loadHTMLString(_ htmlContent: String!, baseURL: URL!, isNextChapter: Bool) {
 
 148         // Insert the stored highlights to the HTML
 
 149         if self.anchorTapped { // dont scroll to bottom, when anchor tapped,
 
 150             self.anchorTapped = false
 
 151             self.isNextChapter = true
 
 154             self.isNextChapter = isNextChapter
 
 156         let tempHtmlContent = htmlContentWithInsertHighlights(htmlContent)
 
 157         // Load the html into the webview
 
 159         self.pageLoaded = false
 
 160         print("pageStartLoading")
 
 163         webView?.loadHTMLString(tempHtmlContent, baseURL: baseURL)
 
 166     // MARK: - Highlights
 
 168     fileprivate func htmlContentWithInsertHighlights(_ htmlContent: String) -> String {
 
 169         var tempHtmlContent = htmlContent as NSString
 
 170         // Restore highlights
 
 171         guard let bookId = (self.book.name as NSString?)?.deletingPathExtension else {
 
 172             return tempHtmlContent as String
 
 175         let highlights = Highlight.allByBookId(withConfiguration: self.readerConfig, bookId: bookId, andPage: pageNumber as NSNumber?)
 
 177         if (highlights.count > 0) {
 
 178             for item in highlights {
 
 179                 let style = HighlightStyle.classForStyle(item.type)
 
 180                 let tag = "<highlight id=\"\(item.highlightId!)\" onclick=\"callHighlightURL(this);\" class=\"\(style)\">\(item.content!)</highlight>"
 
 181                 var locator = item.contentPre + item.content
 
 182                 locator += item.contentPost
 
 183                 locator = Highlight.removeSentenceSpam(locator) /// Fix for Highlights
 
 184                 let range: NSRange = tempHtmlContent.range(of: locator, options: .literal)
 
 186                 if range.location != NSNotFound {
 
 187                     let newRange = NSRange(location: range.location + item.contentPre.count, length: item.content.count)
 
 188                     tempHtmlContent = tempHtmlContent.replacingCharacters(in: newRange, with: tag) as NSString
 
 190                     print("highlight range not found")
 
 194         return tempHtmlContent as String
 
 197     // MARK: - UIWebView Delegate
 
 199     open func webViewDidFinishLoad(_ webView: UIWebView) {
 
 200         guard let webView = webView as? FolioReaderWebView else {
 
 204         let anchorFromURL = self.folioReader.readerCenter?.anchor
 
 205         if anchorFromURL != nil {
 
 206             handleAnchor(anchorFromURL!, avoidBeginningAnchors: false, animated: true)
 
 207             self.folioReader.readerCenter?.anchor = nil
 
 210         delegate?.pageWillLoad?(self)
 
 212         // Add the custom class based onClick listener
 
 213         self.setupClassBasedOnClickListeners()
 
 217         if self.readerConfig.enableTTS && !self.book.hasAudio {
 
 218             webView.js("wrappingSentencesWithinPTags()")
 
 220             if let audioPlayer = self.folioReader.readerAudioPlayer, (audioPlayer.isPlaying() == true) {
 
 221                 audioPlayer.readCurrentSentence()
 
 226         let direction: ScrollDirection = self.folioReader.needsRTLChange ? .positive(withConfiguration: self.readerConfig) : .negative(withConfiguration: self.readerConfig)
 
 227         print("direction \(direction), pageScrollDirection \(self.folioReader.readerCenter?.pageScrollDirection)")
 
 228         //TODO: TUTAJ PORAWIAC. scrollPageToBottom() powinno wywoływać się tylko gdy scrollujemy w górę do następnego chaptera i gdy scrollujemy w lewo do następnego chaptera
 
 229         if (self.folioReader.readerCenter?.pageScrollDirection == direction &&
 
 230             self.folioReader.readerCenter?.isScrolling == true &&
 
 231             self.readerConfig.scrollDirection != .horizontalWithVerticalContent) {
 
 233             print("scrollPageToBottom()")
 
 238             print("no scrollPageToBottom()")
 
 246         UIView.animate(withDuration: 0.2, animations: {webView.alpha = 1}, completion: { finished in
 
 247             webView.isColors = false
 
 248             self.webView?.createMenu(options: false)
 
 252         print("pageEndLoading")
 
 253         self.pageLoaded = true
 
 255         delegate?.pageDidLoad?(self)
 
 258     open func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
 
 260             let webView = webView as? FolioReaderWebView,
 
 261             let scheme = request.url?.scheme else {
 
 265         guard let url = request.url else { return false }
 
 267         if scheme == "highlight" {
 
 269             shouldShowBar = false
 
 271             guard let decoded = url.absoluteString.removingPercentEncoding else { return false }
 
 272             let index = decoded.index(decoded.startIndex, offsetBy: 12)
 
 273             let rect = CGRectFromString(String(decoded[index...]))
 
 275             webView.createMenu(options: true)
 
 276             webView.setMenuVisible(true, andRect: rect)
 
 280         } else if scheme == "play-audio" {
 
 281             guard let decoded = url.absoluteString.removingPercentEncoding else { return false }
 
 282             let index = decoded.index(decoded.startIndex, offsetBy: 13)
 
 283             let playID = String(decoded[index...])
 
 284             let chapter = self.folioReader.readerCenter?.getCurrentChapter()
 
 285             let href = chapter?.href ?? ""
 
 286             self.folioReader.readerAudioPlayer?.playAudio(href, fragmentID: playID)
 
 289         } else if scheme == "file" {
 
 291             let anchorFromURL = url.fragment
 
 293             // Handle internal url
 
 294             if !url.pathExtension.isEmpty {
 
 295                 let pathComponent = (self.book.opfResource.href as NSString?)?.deletingLastPathComponent
 
 296                 guard let base = ((pathComponent == nil || pathComponent?.isEmpty == true) ? self.book.name : pathComponent) else {
 
 301                 let splitedPath = path.components(separatedBy: base)
 
 303                 // Return to avoid crash
 
 304                 if (splitedPath.count <= 1 || splitedPath[1].isEmpty) {
 
 308                 let href = splitedPath[1].trimmingCharacters(in: CharacterSet(charactersIn: "/"))
 
 309                 let hrefPage = (self.folioReader.readerCenter?.findPageByHref(href) ?? 0) + 1
 
 311                 if (hrefPage == pageNumber) {
 
 312                     // Handle internal #anchor
 
 313                     if anchorFromURL != nil {
 
 314                         handleAnchor(anchorFromURL!, avoidBeginningAnchors: false, animated: true)
 
 319                     self.folioReader.readerCenter?.anchor = anchorFromURL
 
 321                     self.folioReader.readerCenter?.changePageWith(href: href, animated: true)
 
 326             // Handle internal #anchor
 
 327             if anchorFromURL != nil {
 
 328                 handleAnchor(anchorFromURL!, avoidBeginningAnchors: false, animated: true)
 
 333         } else if scheme == "mailto" {
 
 336         } else if url.absoluteString != "about:blank" && scheme.contains("http") && navigationType == .linkClicked {
 
 338             if #available(iOS 9.0, *) {
 
 339                 let safariVC = SFSafariViewController(url: request.url!)
 
 340                 safariVC.view.tintColor = self.readerConfig.tintColor
 
 341                 self.folioReader.readerCenter?.present(safariVC, animated: true, completion: nil)
 
 343                 let webViewController = WebViewController(url: request.url!)
 
 344                 let nav = UINavigationController(rootViewController: webViewController)
 
 345                 nav.view.tintColor = self.readerConfig.tintColor
 
 346                 self.folioReader.readerCenter?.present(nav, animated: true, completion: nil)
 
 350             // Check if the url is a custom class based onClick listerner
 
 351             var isClassBasedOnClickListenerScheme = false
 
 352             for listener in self.readerConfig.classBasedOnClickListeners {
 
 354                 if scheme == listener.schemeName,
 
 355                     let absoluteURLString = request.url?.absoluteString,
 
 356                     let range = absoluteURLString.range(of: "/clientX=") {
 
 357                     let baseURL = String(absoluteURLString[..<range.lowerBound])
 
 358                     let positionString = String(absoluteURLString[range.lowerBound...])
 
 359                     if let point = getEventTouchPoint(fromPositionParameterString: positionString) {
 
 360                         let attributeContentString = (baseURL.replacingOccurrences(of: "\(scheme)://", with: "").removingPercentEncoding)
 
 361                         // Call the on click action block
 
 362                         listener.onClickAction(attributeContentString, point)
 
 363                         // Mark the scheme as class based click listener scheme
 
 364                         isClassBasedOnClickListenerScheme = true
 
 369             if isClassBasedOnClickListenerScheme == false {
 
 370                 // Try to open the url with the system if it wasn't a custom class based click listener
 
 371                 if UIApplication.shared.canOpenURL(url) {
 
 372                     UIApplication.shared.openURL(url)
 
 383     fileprivate func getEventTouchPoint(fromPositionParameterString positionParameterString: String) -> CGPoint? {
 
 384         // Remove the parameter names: "/clientX=188&clientY=292" -> "188&292"
 
 385         var positionParameterString = positionParameterString.replacingOccurrences(of: "/clientX=", with: "")
 
 386         positionParameterString = positionParameterString.replacingOccurrences(of: "clientY=", with: "")
 
 387         // Separate both position values into an array: "188&292" -> [188],[292]
 
 388         let positionStringValues = positionParameterString.components(separatedBy: "&")
 
 389         // Multiply the raw positions with the screen scale and return them as CGPoint
 
 391             positionStringValues.count == 2,
 
 392             let xPos = Int(positionStringValues[0]),
 
 393             let yPos = Int(positionStringValues[1]) {
 
 394             return CGPoint(x: xPos, y: yPos)
 
 399     // MARK: Gesture recognizer
 
 401     open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
 
 402         if gestureRecognizer.view is FolioReaderWebView {
 
 403             if otherGestureRecognizer is UILongPressGestureRecognizer {
 
 404                 if UIMenuController.shared.isMenuVisible {
 
 405                     webView?.setMenuVisible(false)
 
 414     @objc open func handleTapGesture(_ recognizer: UITapGestureRecognizer) {
 
 415         self.delegate?.pageTap?(recognizer)
 
 417         if let _navigationController = self.folioReader.readerCenter?.navigationController, (_navigationController.isNavigationBarHidden == true) {
 
 418             let selected = webView?.js("getSelectedText()")
 
 420             guard (selected == nil || selected?.isEmpty == true) else {
 
 424             let delay = 0.4 * Double(NSEC_PER_SEC) // 0.4 seconds * nanoseconds per seconds
 
 425             let dispatchTime = (DispatchTime.now() + (Double(Int64(delay)) / Double(NSEC_PER_SEC)))
 
 427             DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: {
 
 428                 if (self.shouldShowBar == true && self.menuIsVisible == false) {
 
 429                     self.folioReader.readerCenter?.toggleBars()
 
 432         } else if (self.readerConfig.shouldHideNavigationOnTap == true) {
 
 433             self.folioReader.readerCenter?.hideBars()
 
 434             self.menuIsVisible = false
 
 438     // MARK: - Public scroll postion setter
 
 441      Scrolls the page to a given offset
 
 443      - parameter offset:   The offset to scroll
 
 444      - parameter animated: Enable or not scrolling animation
 
 446     open func scrollPageToOffset(_ offset: CGFloat, animated: Bool) {
 
 447         let pageOffsetPoint = self.readerConfig.isDirection(CGPoint(x: 0, y: offset), CGPoint(x: offset, y: 0), CGPoint(x: 0, y: offset))
 
 448         webView?.scrollView.setContentOffset(pageOffsetPoint, animated: animated)
 
 452      Scrolls the page to bottom
 
 454     open func scrollPageToBottom() {
 
 455         guard let webView = webView else { return }
 
 456         let bottomOffset = self.readerConfig.isDirection(
 
 457             CGPoint(x: 0, y: webView.scrollView.contentSize.height - webView.scrollView.bounds.height),
 
 458             CGPoint(x: webView.scrollView.contentSize.width - webView.scrollView.bounds.width, y: 0),
 
 459             CGPoint(x: webView.scrollView.contentSize.width - webView.scrollView.bounds.width, y: 0)
 
 462         if bottomOffset.forDirection(withConfiguration: self.readerConfig) >= 0 {
 
 463             DispatchQueue.main.async {
 
 464                 self.webView?.scrollView.setContentOffset(bottomOffset, animated: false)
 
 470      Handdle #anchors in html, get the offset and scroll to it
 
 472      - parameter anchor:                The #anchor
 
 473      - parameter avoidBeginningAnchors: Sometimes the anchor is on the beggining of the text, there is not need to scroll
 
 474      - parameter animated:              Enable or not scrolling animation
 
 476     open func handleAnchor(_ anchor: String,  avoidBeginningAnchors: Bool, animated: Bool) {
 
 478             let offset = getAnchorOffset(anchor)
 
 480             switch self.readerConfig.scrollDirection {
 
 481             case .vertical, .defaultVertical:
 
 482                 let isBeginning = (offset < frame.forDirection(withConfiguration: self.readerConfig) * 0.5)
 
 484                 if !avoidBeginningAnchors {
 
 485                     scrollPageToOffset(offset, animated: animated)
 
 486                 } else if avoidBeginningAnchors && !isBeginning {
 
 487                     scrollPageToOffset(offset, animated: animated)
 
 489             case .horizontal, .horizontalWithVerticalContent:
 
 490                 scrollPageToOffset(offset, animated: animated)
 
 498      Get the #anchor offset in the page
 
 500      - parameter anchor: The #anchor id
 
 501      - returns: The element offset ready to scroll
 
 503     func getAnchorOffset(_ anchor: String) -> CGFloat {
 
 504         let horizontal = self.readerConfig.scrollDirection == .horizontal
 
 507         if let strOffset = webView?.js("getAnchorOffset('\(anchor)', \(horizontal.description))") {
 
 508             return CGFloat((strOffset as NSString).floatValue)
 
 517      Audio Mark ID - marks an element with an ID with the given class and scrolls to it
 
 519      - parameter identifier: The identifier
 
 521     func audioMarkID(_ identifier: String) {
 
 522         guard let currentPage = self.folioReader.readerCenter?.currentPage else {
 
 526         let playbackActiveClass = self.book.playbackActiveClass
 
 527         currentPage.webView?.js("audioMarkID('\(playbackActiveClass)','\(identifier)')")
 
 530     // MARK: UIMenu visibility
 
 532     override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
 
 533         guard let webView = webView else { return false }
 
 535         if UIMenuController.shared.menuItems?.count == 0 {
 
 536             webView.isColors = false
 
 537             webView.createMenu(options: false)
 
 540         if !webView.isShare && !webView.isColors {
 
 541             if let result = webView.js("getSelectedText()") , result.components(separatedBy: " ").count == 1 {
 
 542                 webView.isOneWord = true
 
 543                 webView.createMenu(options: false)
 
 545                 webView.isOneWord = false
 
 549         return super.canPerformAction(action, withSender: sender)
 
 552     // MARK: ColorView fix for horizontal layout
 
 553     @objc func refreshPageMode() {
 
 554         guard let webView = webView else { return }
 
 556         if (self.folioReader.nightMode == true) {
 
 557             // omit create webView and colorView
 
 558             let script = "document.documentElement.offsetHeight"
 
 559             let contentHeight = webView.stringByEvaluatingJavaScript(from: script)
 
 560             let frameHeight = webView.frame.height
 
 561             let lastPageHeight = frameHeight * CGFloat(webView.pageCount) - CGFloat(Double(contentHeight!)!)
 
 562             colorView.frame = CGRect(x: webView.frame.width * CGFloat(webView.pageCount-1), y: webView.frame.height - lastPageHeight, width: webView.frame.width, height: lastPageHeight)
 
 564             colorView.frame = CGRect.zero
 
 568     // MARK: - Class based click listener
 
 570     fileprivate func setupClassBasedOnClickListeners() {
 
 571         for listener in self.readerConfig.classBasedOnClickListeners {
 
 572             self.webView?.js("addClassBasedOnClickListener(\"\(listener.schemeName)\", \"\(listener.querySelector)\", \"\(listener.attributeName)\", \"\(listener.selectAll)\")");
 
 576     // MARK: - Public Java Script injection
 
 579      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.
 
 581      - returns: The result of running the JavaScript script passed in the script parameter, or nil if the script fails.
 
 583     open func performJavaScript(_ javaScriptCode: String) -> String? {
 
 584         return webView?.js(javaScriptCode)