From a5636e0b60e07c1b90a967cca6dedf8c31292e5e Mon Sep 17 00:00:00 2001 From: Chris Li Date: Thu, 5 Jan 2017 13:27:31 -0500 Subject: [PATCH] table of content --- Kiwix-iOS/Controller/Main/JSInjection.swift | 16 +- Kiwix-iOS/Controller/Main/MainDelegates.swift | 29 ++- .../Others/TableOfContentsController.swift | 86 ++++---- Kiwix/JavaScripts/injection.js | 186 +++++------------- 4 files changed, 122 insertions(+), 195 deletions(-) diff --git a/Kiwix-iOS/Controller/Main/JSInjection.swift b/Kiwix-iOS/Controller/Main/JSInjection.swift index 1ec4ba1c..59afe71b 100644 --- a/Kiwix-iOS/Controller/Main/JSInjection.swift +++ b/Kiwix-iOS/Controller/Main/JSInjection.swift @@ -42,16 +42,16 @@ class JS { // MARK: - Table of Contents - class func startTOCCallBack(_ webView: UIWebView) { - webView.stringByEvaluatingJavaScript(from: "startCallBack()") + class func startTOCCallBack(webView: UIWebView) { + webView.stringByEvaluatingJavaScript(from: "tableOfContents.startCallBack()") } - class func stopTOCCallBack(_ webView: UIWebView) { - webView.stringByEvaluatingJavaScript(from: "stopCallBack()") + class func stopTOCCallBack(webView: UIWebView) { + webView.stringByEvaluatingJavaScript(from: "tableOfContents.stopCallBack()") } - class func getTableOfContents(from webView: UIWebView) -> [HTMLHeading] { - let jString = "getTableOfContents().headerObjects;" + class func getTableOfContents(webView: UIWebView) -> [HTMLHeading] { + let jString = "tableOfContents.getHeadingObjects()" guard let elements = webView.context.evaluateScript(jString).toArray() as? [[String: Any]] else {return [HTMLHeading]()} var headings = [HTMLHeading]() for element in elements { @@ -61,6 +61,10 @@ class JS { return headings } + class func scrollToHeading(webView: UIWebView, index: Int) { + webView.stringByEvaluatingJavaScript(from: "tableOfContents.scrollToView(\(index))") + } + } extension UIWebView { diff --git a/Kiwix-iOS/Controller/Main/MainDelegates.swift b/Kiwix-iOS/Controller/Main/MainDelegates.swift index 9b121afd..45f4e0fb 100644 --- a/Kiwix-iOS/Controller/Main/MainDelegates.swift +++ b/Kiwix-iOS/Controller/Main/MainDelegates.swift @@ -16,13 +16,23 @@ import CloudKit extension MainController: UIWebViewDelegate, SFSafariViewControllerDelegate { func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool { guard let url = request.url else {return false} - guard url.isKiwixURL else { + if url.isKiwixURL { + return true + } else if url.scheme == "pagescroll" { + let components = URLComponents(string: url.absoluteString) + guard let query = components?.queryItems, + let startStr = query[0].value, let start = Int(startStr), + let lengthStr = query[1].value, let length = Int(lengthStr) else { + return false + } + tableOfContentsController?.visibleRange = (start, length) + return false + } else { let controller = SFSafariViewController(url: url) controller.delegate = self present(controller, animated: true, completion: nil) return false } - return true } func webViewDidStartLoad(_ webView: UIWebView) { @@ -32,6 +42,8 @@ extension MainController: UIWebViewDelegate, SFSafariViewControllerDelegate { func webViewDidFinishLoad(_ webView: UIWebView) { JS.inject(webView: webView) JS.preventDefaultLongTap(webView: webView) + JS.startTOCCallBack(webView: webView) + URLResponseCache.shared.stop() guard let url = webView.request?.url, @@ -155,7 +167,7 @@ extension MainController: ButtonDelegates { } func didTapTOCButton() { - tableOfContentsController?.headings = JS.getTableOfContents(from: webView) + tableOfContentsController?.headings = JS.getTableOfContents(webView: webView) isShowingTableOfContents ? hideTableOfContents(animated: true) : showTableOfContents(animated: true) } @@ -228,8 +240,6 @@ extension MainController: TableOfContentsDelegate { view.layoutIfNeeded() dimView.alpha = 0.5 } - - JS.startTOCCallBack(webView) } func hideTableOfContents(animated: Bool) { @@ -251,8 +261,6 @@ extension MainController: TableOfContentsDelegate { dimView.isHidden = true tocVisiualEffectView.isHidden = true } - - JS.stopTOCCallBack(webView) } func configureTOCConstraints() { @@ -274,8 +282,11 @@ extension MainController: TableOfContentsDelegate { } } - func didSelectTOCItem(heading: HTMLHeading) { - + func didSelectHeading(index: Int) { + JS.scrollToHeading(webView: webView, index: index) + if traitCollection.horizontalSizeClass == .compact { + hideTableOfContents(animated: true) + } } @IBAction func didTapTOCDimView(_ sender: UITapGestureRecognizer) { diff --git a/Kiwix-iOS/Controller/Others/TableOfContentsController.swift b/Kiwix-iOS/Controller/Others/TableOfContentsController.swift index 343eed9a..5e6fde6f 100644 --- a/Kiwix-iOS/Controller/Others/TableOfContentsController.swift +++ b/Kiwix-iOS/Controller/Others/TableOfContentsController.swift @@ -15,20 +15,17 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV private let visibleHeaderIndicator = UIView() weak var delegate: TableOfContentsDelegate? - private var headinglevelMin = 0 var headings = [HTMLHeading]() { didSet { configurePreferredContentSize() - headinglevelMin = max(2, headings.map({$0.level}).min() ?? 0) - visibleHeaderIDs.removeAll() tableView.reloadData() } } - var visibleHeaderIDs = [String]() { + + var visibleRange: (start: Int, length: Int)? { didSet { - guard oldValue != visibleHeaderIDs else {return} - configureVisibleHeaderView(animated: oldValue.count > 0) + configureVisibleHeaderView(animated: true) } } @@ -50,43 +47,40 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV } func configureVisibleHeaderView(animated: Bool) { -// // no visible header -// guard visibleHeaderIDs.count > 0 else { -// visibleHeaderIndicator.isHidden = true -// return -// } -// -// // calculations -// guard let minIndex = headings.index(where: {$0.id == visibleHeaderIDs.first}), -// let maxIndex = headings.index(where: {$0.id == visibleHeaderIDs.last}) else {return} -// let topIndexPath = IndexPath(row: minIndex, section: 0) -// let bottomIndexPath = IndexPath(row: maxIndex, section: 0) -// let topCellFrame = tableView.rectForRow(at: topIndexPath) -// let bottomCellFrame = tableView.rectForRow(at: bottomIndexPath) -// let top = topCellFrame.origin.y + topCellFrame.height * 0.1 -// let bottom = bottomCellFrame.origin.y + bottomCellFrame.height * 0.9 -// -// // indicator frame -// visibleHeaderIndicator.isHidden = false -// if animated { -// UIView.animate(withDuration: 0.1, animations: { -// self.visibleHeaderIndicator.frame = CGRect(x: 0, y: top, width: 3, height: bottom - top) -// }) -// } else { -// visibleHeaderIndicator.frame = CGRect(x: 0, y: top, width: 3, height: bottom - top) -// } -// -// // tableview scroll -// let topCellVisible = tableView.indexPathsForVisibleRows?.contains(topIndexPath) ?? false -// let bottomCellVisible = tableView.indexPathsForVisibleRows?.contains(bottomIndexPath) ?? false -// switch (topCellVisible, bottomCellVisible) { -// case (true, false): -// tableView.scrollToRow(at: bottomIndexPath, at: .bottom, animated: animated) -// case (false, true), (false, false): -// tableView.scrollToRow(at: topIndexPath, at: .top, animated: animated) -// default: -// return -// } + // no visible header + guard let visibleRange = visibleRange else { + visibleHeaderIndicator.isHidden = true + return + } + + let topIndexPath = IndexPath(row: visibleRange.start, section: 0) + let bottomIndexPath = IndexPath(row: visibleRange.start + visibleRange.length - 1, section: 0) + let topCellFrame = tableView.rectForRow(at: topIndexPath) + let bottomCellFrame = tableView.rectForRow(at: bottomIndexPath) + let top = topCellFrame.origin.y + topCellFrame.height * 0.1 + let bottom = bottomCellFrame.origin.y + bottomCellFrame.height * 0.9 + + // indicator frame + visibleHeaderIndicator.isHidden = false + if animated { + UIView.animate(withDuration: 0.1, animations: { + self.visibleHeaderIndicator.frame = CGRect(x: 0, y: top, width: 3, height: bottom - top) + }) + } else { + visibleHeaderIndicator.frame = CGRect(x: 0, y: top, width: 3, height: bottom - top) + } + + // tableview scroll + let topCellVisible = tableView.indexPathsForVisibleRows?.contains(topIndexPath) ?? false + let bottomCellVisible = tableView.indexPathsForVisibleRows?.contains(bottomIndexPath) ?? false + switch (topCellVisible, bottomCellVisible) { + case (true, false): + tableView.scrollToRow(at: bottomIndexPath, at: .bottom, animated: animated) + case (false, true), (false, false): + tableView.scrollToRow(at: topIndexPath, at: .top, animated: animated) + default: + return + } } // MARK: - Table view data source @@ -113,7 +107,7 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV default: let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.textLabel?.text = heading.textContent - cell.indentationLevel = max(0, (heading.level - headinglevelMin) * 2) + cell.indentationLevel = (heading.level - 2) * 2 return cell } } @@ -121,7 +115,7 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV // MARK: - Table view delegate func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - delegate?.didSelectTOCItem(heading: headings[indexPath.row]) + delegate?.didSelectHeading(index: indexPath.row) tableView.deselectRow(at: indexPath, animated: true) } @@ -145,5 +139,5 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV } protocol TableOfContentsDelegate: class { - func didSelectTOCItem(heading: HTMLHeading) + func didSelectHeading(index: Int) } diff --git a/Kiwix/JavaScripts/injection.js b/Kiwix/JavaScripts/injection.js index e275746d..2d894f4f 100644 --- a/Kiwix/JavaScripts/injection.js +++ b/Kiwix/JavaScripts/injection.js @@ -1,149 +1,67 @@ -var toc = undefined; -var visEle = undefined; -var visibleHeaderIDs = undefined; - -function getTableOfContents() { - toc = toc == undefined ? new TableOfContents() : toc; - return toc; -} - function TableOfContents () { - this.getHeaderElements = function () { - var h1 = document.getElementsByTagName('h1')[0]; - var elememts = Array.prototype.slice.call(document.querySelectorAll('h2, h3, h4')); - elememts.splice(0, 0, h1); - return elememts; - } - - this.headerElements = this.getHeaderElements(); - - this.getHeaderObjects = function () { - return this.headerElements.map( elementToObject ); - } - - this.headerObjects = this.getHeaderObjects(); - - function elementToObject(element, index) { - var obj = {}; - obj.id = element.id; - obj.index = index; - obj.textContent = element.textContent; - obj.tagName = element.tagName; - return obj; - } -} + this.headings = document.querySelectorAll("h1, h2, h3, h4, h5, h6"); -function getVisibleElementsChecker() { - visEle = visEle == undefined ? new VisibleElements() : visEle; - return visEle; -} - -function VisibleElements () { - // return a 2d array [[header(h1/h2/h3), p, ul, div]] - function getElementGroups() { - var groups = []; - var group = [document.getElementsByTagName('h1')[0]]; - var contents = document.getElementById("mw-content-text").children; - var headerTags = ['h2', 'h3', 'h4'].map(function(x){ return x.toUpperCase() }); - - for (i = 0; i < contents.length; i++) { - var element = contents[i]; - if (headerTags.includes(element.tagName)) { - groups.push(group); - group = [] - } - group.push(element); + this.getHeadingObjects = function () { + var headings = []; + for (i = 0; i < this.headings.length; i++) { + var element = this.headings[i]; + var obj = {}; + obj.id = element.id; + obj.index = i; + obj.textContent = element.textContent; + obj.tagName = element.tagName; + headings.push(obj); } - - groups.push(group); - return groups; + return headings; } - - this.elementGroups = getElementGroups(); - - this.getVisibleHeaders = function () { - var groups = this.elementGroups; + + this.scrollToView = function (index) { + this.headings[index].scrollIntoView(); + } + + this.getVisibleHeadingIndex = function () { var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight); - var visibleHeaders = []; - - for (i = 0; i < groups.length; i++) { - var group = groups[i]; - var header = group[0]; - var groupVisible = false; - - for (j = 0; j < group.length; j++) { - var element = group[j]; - var rect = element.getBoundingClientRect(); - var isVisible = !(rect.bottom < 0 || rect.top - viewHeight >= 0); - groupVisible = groupVisible || isVisible; - if (isVisible) { // if found an element visible in group, break and check the next group - visibleHeaders.push(header) - break; - } - } - - // If found visible groups already, but current group is not visible - // It means we are checking area below the visible area, should break - if (visibleHeaders.length > 0 && !groupVisible) { - break; + var aboveIndexes = []; + var visibleIndexes = []; + var belowIndexes = []; + + for (i = 0; i < this.headings.length; i++) { + var element = this.headings[i]; + var rect = element.getBoundingClientRect(); + + var isAboveTopBorder = rect.bottom < 0; + var isBelowBottomBorder = viewHeight - rect.top < 0; + + if (isAboveTopBorder) { + aboveIndexes.push(i); + } else if (isBelowBottomBorder) { + belowIndexes.push(i); + } else { + visibleIndexes.push(i); } } - - return visibleHeaders; - } - - this.getVisibleHeaderIDs = function () { - return this.getVisibleHeaders().map(function(x){ return x.id }); - } -} -function startCallBack() { - function arraysEqual(a, b) { - if (a === b) return true; - if (a == null || b == null) return false; - if (a.length != b.length) return false; - - for (var i = 0; i < a.length; ++i) { - if (a[i] !== b[i]) return false; + if (aboveIndexes.length > 0) { + return [aboveIndexes[aboveIndexes.length-1]].concat(visibleIndexes); + } else { + return visibleIndexes; } - return true; } - function callBack(visibleHeaderIDs) { - var parameter = visibleHeaderIDs.map(function(x){ return 'header=' + x }).join('&'); - window.location = 'pagescroll:scrollEnded?' + parameter; - } - - visibleHeaderIDs = getVisibleElementsChecker().getVisibleHeaderIDs(); - window.onscroll = function() { - var newVisibleHeaderIDs = getVisibleElementsChecker().getVisibleHeaderIDs(); - if (!arraysEqual(visibleHeaderIDs, newVisibleHeaderIDs)) { - visibleHeaderIDs = newVisibleHeaderIDs; - callBack(visibleHeaderIDs); - } - }; - callBack(visibleHeaderIDs); -} - -function stopCallBack() { - window.onscroll = undefined; -} - -function getSnippet() { - var element = document.getElementById('mw-content-text'); - if (element) { - var children = element.children; - for (i = 0; i < children.length; i++) { - var child = children[i]; - if (child.tagName == 'P') { - var text = child.textContent || child.innerText || ""; - if (text.replace(/\s/g, '').length) { - var regex = /\[[0-9|a-z|A-Z| ]*\]/g; - text = text.replace(regex, ""); - return text; - } + this.startCallBack = function () { + var handleScroll = function() { + var indexes = tableOfContents.getVisibleHeadingIndex(); + if (indexes.length > 0) { + window.location = 'pagescroll:scrollEnded?start=' + indexes[0] + '&length=' + indexes.length; } } + window.onscroll = handleScroll; + handleScroll(); + } + + this.stopCallBack = function () { + window.onscroll = undefined; } - return null; } + +var tableOfContents = new TableOfContents(); \ No newline at end of file