added iOS source code
[wl-app.git] / iOS / Pods / FolioReaderKit / Source / FolioReaderPage.swift
1 //
2 //  FolioReaderPage.swift
3 //  FolioReaderKit
4 //
5 //  Created by Heberti Almeida on 10/04/15.
6 //  Copyright (c) 2015 Folio Reader. All rights reserved.
7 //
8
9 import UIKit
10 import SafariServices
11 import MenuItemKit
12 import JSQWebViewController
13
14 /// Protocol which is used from `FolioReaderPage`s.
15 @objc public protocol FolioReaderPageDelegate: class {
16
17     /**
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`.
19
20      - parameter page: The loaded page
21      */
22     @objc optional func pageWillLoad(_ page: FolioReaderPage)
23
24     /**
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.
26
27      - parameter page: The loaded page
28      */
29     @objc optional func pageDidLoad(_ page: FolioReaderPage)
30     
31     /**
32      Notifies that page receive tap gesture.
33      
34      - parameter recognizer: The tap recognizer
35      */
36     @objc optional func pageTap(_ recognizer: UITapGestureRecognizer)
37 }
38
39 open class FolioReaderPage: UICollectionViewCell, UIWebViewDelegate, UIGestureRecognizerDelegate {
40     weak var delegate: FolioReaderPageDelegate?
41     weak var readerContainer: FolioReaderContainer?
42
43     /// The index of the current page. Note: The index start at 1!
44     open var pageNumber: Int!
45     open var webView: FolioReaderWebView?
46
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
53     
54     fileprivate var readerConfig: FolioReaderConfig {
55         guard let readerContainer = readerContainer else { return FolioReaderConfig() }
56         return readerContainer.readerConfig
57     }
58
59     fileprivate var book: FRBook {
60         guard let readerContainer = readerContainer else { return FRBook() }
61         return readerContainer.book
62     }
63
64     fileprivate var folioReader: FolioReader {
65         guard let readerContainer = readerContainer else { return FolioReader() }
66         return readerContainer.folioReader
67     }
68
69     // MARK: - View life cicle
70
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
76
77         NotificationCenter.default.addObserver(self, selector: #selector(refreshPageMode), name: NSNotification.Name(rawValue: "needRefreshPageMode"), object: nil)
78     }
79
80     public func setup(withReaderContainer readerContainer: FolioReaderContainer) {
81         self.readerContainer = readerContainer
82         guard let readerContainer = self.readerContainer else { return }
83
84         if webView == nil {
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!)
92         }
93         webView?.delegate = self
94
95         if colorView == nil {
96             colorView = UIView()
97             colorView.backgroundColor = self.readerConfig.nightModeBackground
98             webView?.scrollView.addSubview(colorView)
99         }
100
101         // Remove all gestures before adding new one
102         webView?.gestureRecognizers?.forEach({ gesture in
103             webView?.removeGestureRecognizer(gesture)
104         })
105         let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
106         tapGestureRecognizer.numberOfTapsRequired = 1
107         tapGestureRecognizer.delegate = self
108         webView?.addGestureRecognizer(tapGestureRecognizer)
109     }
110
111     required public init?(coder aDecoder: NSCoder) {
112         fatalError("storyboards are incompatible with truth and beauty")
113     }
114
115     deinit {
116         webView?.scrollView.delegate = nil
117         webView?.delegate = nil
118         NotificationCenter.default.removeObserver(self)
119     }
120
121     override open func layoutSubviews() {
122         super.layoutSubviews()
123
124         webView?.setupScrollDirection()
125         webView?.frame = webViewFrame()
126     }
127
128     func webViewFrame() -> CGRect {
129         guard (self.readerConfig.hideBars == false) else {
130             return bounds
131         }
132
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
138
139         return CGRect(
140             x: bounds.origin.x,
141             y: self.readerConfig.isDirection(bounds.origin.y + navTotal, bounds.origin.y + navTotal + paddingTop, bounds.origin.y + navTotal),
142             width: bounds.width,
143             height: self.readerConfig.isDirection(bounds.height - navTotal, bounds.height - navTotal - paddingTop - paddingBottom, bounds.height - navTotal)
144         )
145     }
146
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
152         }
153         else {
154             self.isNextChapter = isNextChapter
155         }
156         let tempHtmlContent = htmlContentWithInsertHighlights(htmlContent)
157         // Load the html into the webview
158        
159         self.pageLoaded = false
160         print("pageStartLoading")
161
162         webView?.alpha = 0
163         webView?.loadHTMLString(tempHtmlContent, baseURL: baseURL)
164     }
165
166     // MARK: - Highlights
167
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
173         }
174
175         let highlights = Highlight.allByBookId(withConfiguration: self.readerConfig, bookId: bookId, andPage: pageNumber as NSNumber?)
176
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)
185
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
189                 } else {
190                     print("highlight range not found")
191                 }
192             }
193         }
194         return tempHtmlContent as String
195     }
196
197     // MARK: - UIWebView Delegate
198
199     open func webViewDidFinishLoad(_ webView: UIWebView) {
200         guard let webView = webView as? FolioReaderWebView else {
201             return
202         }
203
204         let anchorFromURL = self.folioReader.readerCenter?.anchor
205         if anchorFromURL != nil {
206             handleAnchor(anchorFromURL!, avoidBeginningAnchors: false, animated: true)
207             self.folioReader.readerCenter?.anchor = nil
208         }
209         
210         delegate?.pageWillLoad?(self)
211
212         // Add the custom class based onClick listener
213         self.setupClassBasedOnClickListeners()
214
215         refreshPageMode()
216
217         if self.readerConfig.enableTTS && !self.book.hasAudio {
218             webView.js("wrappingSentencesWithinPTags()")
219
220             if let audioPlayer = self.folioReader.readerAudioPlayer, (audioPlayer.isPlaying() == true) {
221                 audioPlayer.readCurrentSentence()
222             }
223         }
224
225        /*
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) {
232             
233             print("scrollPageToBottom()")
234
235             scrollPageToBottom()
236         }
237         else {
238             print("no scrollPageToBottom()")
239
240         }
241  */
242         if !isNextChapter {
243             scrollPageToBottom()
244         }
245
246         UIView.animate(withDuration: 0.2, animations: {webView.alpha = 1}, completion: { finished in
247             webView.isColors = false
248             self.webView?.createMenu(options: false)
249         })
250
251         
252         print("pageEndLoading")
253         self.pageLoaded = true
254         
255         delegate?.pageDidLoad?(self)
256     }
257
258     open func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
259         guard
260             let webView = webView as? FolioReaderWebView,
261             let scheme = request.url?.scheme else {
262                 return true
263         }
264
265         guard let url = request.url else { return false }
266
267         if scheme == "highlight" {
268
269             shouldShowBar = false
270
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...]))
274
275             webView.createMenu(options: true)
276             webView.setMenuVisible(true, andRect: rect)
277             menuIsVisible = true
278
279             return false
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)
287
288             return false
289         } else if scheme == "file" {
290
291             let anchorFromURL = url.fragment
292
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 {
297                     return true
298                 }
299
300                 let path = url.path
301                 let splitedPath = path.components(separatedBy: base)
302
303                 // Return to avoid crash
304                 if (splitedPath.count <= 1 || splitedPath[1].isEmpty) {
305                     return true
306                 }
307
308                 let href = splitedPath[1].trimmingCharacters(in: CharacterSet(charactersIn: "/"))
309                 let hrefPage = (self.folioReader.readerCenter?.findPageByHref(href) ?? 0) + 1
310
311                 if (hrefPage == pageNumber) {
312                     // Handle internal #anchor
313                     if anchorFromURL != nil {
314                         handleAnchor(anchorFromURL!, avoidBeginningAnchors: false, animated: true)
315                         return false
316                     }
317                 } else {
318                     anchorTapped = true
319                     self.folioReader.readerCenter?.anchor = anchorFromURL
320
321                     self.folioReader.readerCenter?.changePageWith(href: href, animated: true)
322                 }
323                 return false
324             }
325
326             // Handle internal #anchor
327             if anchorFromURL != nil {
328                 handleAnchor(anchorFromURL!, avoidBeginningAnchors: false, animated: true)
329                 return false
330             }
331
332             return true
333         } else if scheme == "mailto" {
334             print("Email")
335             return true
336         } else if url.absoluteString != "about:blank" && scheme.contains("http") && navigationType == .linkClicked {
337
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)
342             } else {
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)
347             }
348             return false
349         } else {
350             // Check if the url is a custom class based onClick listerner
351             var isClassBasedOnClickListenerScheme = false
352             for listener in self.readerConfig.classBasedOnClickListeners {
353
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
365                     }
366                 }
367             }
368
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)
373                     return false
374                 }
375             } else {
376                 return false
377             }
378         }
379
380         return true
381     }
382
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
390         if
391             positionStringValues.count == 2,
392             let xPos = Int(positionStringValues[0]),
393             let yPos = Int(positionStringValues[1]) {
394             return CGPoint(x: xPos, y: yPos)
395         }
396         return nil
397     }
398
399     // MARK: Gesture recognizer
400
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)
406                 }
407                 return false
408             }
409             return true
410         }
411         return false
412     }
413
414     @objc open func handleTapGesture(_ recognizer: UITapGestureRecognizer) {
415         self.delegate?.pageTap?(recognizer)
416         
417         if let _navigationController = self.folioReader.readerCenter?.navigationController, (_navigationController.isNavigationBarHidden == true) {
418             let selected = webView?.js("getSelectedText()")
419             
420             guard (selected == nil || selected?.isEmpty == true) else {
421                 return
422             }
423
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)))
426             
427             DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: {
428                 if (self.shouldShowBar == true && self.menuIsVisible == false) {
429                     self.folioReader.readerCenter?.toggleBars()
430                 }
431             })
432         } else if (self.readerConfig.shouldHideNavigationOnTap == true) {
433             self.folioReader.readerCenter?.hideBars()
434             self.menuIsVisible = false
435         }
436     }
437
438     // MARK: - Public scroll postion setter
439
440     /**
441      Scrolls the page to a given offset
442
443      - parameter offset:   The offset to scroll
444      - parameter animated: Enable or not scrolling animation
445      */
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)
449     }
450
451     /**
452      Scrolls the page to bottom
453      */
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)
460         )
461
462         if bottomOffset.forDirection(withConfiguration: self.readerConfig) >= 0 {
463             DispatchQueue.main.async {
464                 self.webView?.scrollView.setContentOffset(bottomOffset, animated: false)
465             }
466         }
467     }
468
469     /**
470      Handdle #anchors in html, get the offset and scroll to it
471
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
475      */
476     open func handleAnchor(_ anchor: String,  avoidBeginningAnchors: Bool, animated: Bool) {
477         if !anchor.isEmpty {
478             let offset = getAnchorOffset(anchor)
479
480             switch self.readerConfig.scrollDirection {
481             case .vertical, .defaultVertical:
482                 let isBeginning = (offset < frame.forDirection(withConfiguration: self.readerConfig) * 0.5)
483
484                 if !avoidBeginningAnchors {
485                     scrollPageToOffset(offset, animated: animated)
486                 } else if avoidBeginningAnchors && !isBeginning {
487                     scrollPageToOffset(offset, animated: animated)
488                 }
489             case .horizontal, .horizontalWithVerticalContent:
490                 scrollPageToOffset(offset, animated: animated)
491             }
492         }
493     }
494
495     // MARK: Helper
496
497     /**
498      Get the #anchor offset in the page
499
500      - parameter anchor: The #anchor id
501      - returns: The element offset ready to scroll
502      */
503     func getAnchorOffset(_ anchor: String) -> CGFloat {
504         let horizontal = self.readerConfig.scrollDirection == .horizontal
505         
506
507         if let strOffset = webView?.js("getAnchorOffset('\(anchor)', \(horizontal.description))") {
508             return CGFloat((strOffset as NSString).floatValue)
509         }
510
511         return CGFloat(0)
512     }
513
514     // MARK: Mark ID
515
516     /**
517      Audio Mark ID - marks an element with an ID with the given class and scrolls to it
518
519      - parameter identifier: The identifier
520      */
521     func audioMarkID(_ identifier: String) {
522         guard let currentPage = self.folioReader.readerCenter?.currentPage else {
523             return
524         }
525
526         let playbackActiveClass = self.book.playbackActiveClass
527         currentPage.webView?.js("audioMarkID('\(playbackActiveClass)','\(identifier)')")
528     }
529
530     // MARK: UIMenu visibility
531
532     override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
533         guard let webView = webView else { return false }
534
535         if UIMenuController.shared.menuItems?.count == 0 {
536             webView.isColors = false
537             webView.createMenu(options: false)
538         }
539
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)
544             } else {
545                 webView.isOneWord = false
546             }
547         }
548
549         return super.canPerformAction(action, withSender: sender)
550     }
551
552     // MARK: ColorView fix for horizontal layout
553     @objc func refreshPageMode() {
554         guard let webView = webView else { return }
555
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)
563         } else {
564             colorView.frame = CGRect.zero
565         }
566     }
567     
568     // MARK: - Class based click listener
569     
570     fileprivate func setupClassBasedOnClickListeners() {
571         for listener in self.readerConfig.classBasedOnClickListeners {
572             self.webView?.js("addClassBasedOnClickListener(\"\(listener.schemeName)\", \"\(listener.querySelector)\", \"\(listener.attributeName)\", \"\(listener.selectAll)\")");
573         }
574     }
575     
576     // MARK: - Public Java Script injection
577     
578     /** 
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.
580      
581      - returns: The result of running the JavaScript script passed in the script parameter, or nil if the script fails.
582      */
583     open func performJavaScript(_ javaScriptCode: String) -> String? {
584         return webView?.js(javaScriptCode)
585     }
586 }