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)