table of content

This commit is contained in:
Chris Li 2017-01-05 13:27:31 -05:00
parent fcf6fc46a4
commit a5636e0b60
4 changed files with 122 additions and 195 deletions

View File

@ -42,16 +42,16 @@ class JS {
// MARK: - Table of Contents // MARK: - Table of Contents
class func startTOCCallBack(_ webView: UIWebView) { class func startTOCCallBack(webView: UIWebView) {
webView.stringByEvaluatingJavaScript(from: "startCallBack()") webView.stringByEvaluatingJavaScript(from: "tableOfContents.startCallBack()")
} }
class func stopTOCCallBack(_ webView: UIWebView) { class func stopTOCCallBack(webView: UIWebView) {
webView.stringByEvaluatingJavaScript(from: "stopCallBack()") webView.stringByEvaluatingJavaScript(from: "tableOfContents.stopCallBack()")
} }
class func getTableOfContents(from webView: UIWebView) -> [HTMLHeading] { class func getTableOfContents(webView: UIWebView) -> [HTMLHeading] {
let jString = "getTableOfContents().headerObjects;" let jString = "tableOfContents.getHeadingObjects()"
guard let elements = webView.context.evaluateScript(jString).toArray() as? [[String: Any]] else {return [HTMLHeading]()} guard let elements = webView.context.evaluateScript(jString).toArray() as? [[String: Any]] else {return [HTMLHeading]()}
var headings = [HTMLHeading]() var headings = [HTMLHeading]()
for element in elements { for element in elements {
@ -61,6 +61,10 @@ class JS {
return headings return headings
} }
class func scrollToHeading(webView: UIWebView, index: Int) {
webView.stringByEvaluatingJavaScript(from: "tableOfContents.scrollToView(\(index))")
}
} }
extension UIWebView { extension UIWebView {

View File

@ -16,13 +16,23 @@ import CloudKit
extension MainController: UIWebViewDelegate, SFSafariViewControllerDelegate { extension MainController: UIWebViewDelegate, SFSafariViewControllerDelegate {
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool { func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
guard let url = request.url else {return false} 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) let controller = SFSafariViewController(url: url)
controller.delegate = self controller.delegate = self
present(controller, animated: true, completion: nil) present(controller, animated: true, completion: nil)
return false return false
} }
return true
} }
func webViewDidStartLoad(_ webView: UIWebView) { func webViewDidStartLoad(_ webView: UIWebView) {
@ -32,6 +42,8 @@ extension MainController: UIWebViewDelegate, SFSafariViewControllerDelegate {
func webViewDidFinishLoad(_ webView: UIWebView) { func webViewDidFinishLoad(_ webView: UIWebView) {
JS.inject(webView: webView) JS.inject(webView: webView)
JS.preventDefaultLongTap(webView: webView) JS.preventDefaultLongTap(webView: webView)
JS.startTOCCallBack(webView: webView)
URLResponseCache.shared.stop() URLResponseCache.shared.stop()
guard let url = webView.request?.url, guard let url = webView.request?.url,
@ -155,7 +167,7 @@ extension MainController: ButtonDelegates {
} }
func didTapTOCButton() { func didTapTOCButton() {
tableOfContentsController?.headings = JS.getTableOfContents(from: webView) tableOfContentsController?.headings = JS.getTableOfContents(webView: webView)
isShowingTableOfContents ? hideTableOfContents(animated: true) : showTableOfContents(animated: true) isShowingTableOfContents ? hideTableOfContents(animated: true) : showTableOfContents(animated: true)
} }
@ -228,8 +240,6 @@ extension MainController: TableOfContentsDelegate {
view.layoutIfNeeded() view.layoutIfNeeded()
dimView.alpha = 0.5 dimView.alpha = 0.5
} }
JS.startTOCCallBack(webView)
} }
func hideTableOfContents(animated: Bool) { func hideTableOfContents(animated: Bool) {
@ -251,8 +261,6 @@ extension MainController: TableOfContentsDelegate {
dimView.isHidden = true dimView.isHidden = true
tocVisiualEffectView.isHidden = true tocVisiualEffectView.isHidden = true
} }
JS.stopTOCCallBack(webView)
} }
func configureTOCConstraints() { 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) { @IBAction func didTapTOCDimView(_ sender: UITapGestureRecognizer) {

View File

@ -15,20 +15,17 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV
private let visibleHeaderIndicator = UIView() private let visibleHeaderIndicator = UIView()
weak var delegate: TableOfContentsDelegate? weak var delegate: TableOfContentsDelegate?
private var headinglevelMin = 0
var headings = [HTMLHeading]() { var headings = [HTMLHeading]() {
didSet { didSet {
configurePreferredContentSize() configurePreferredContentSize()
headinglevelMin = max(2, headings.map({$0.level}).min() ?? 0)
visibleHeaderIDs.removeAll()
tableView.reloadData() tableView.reloadData()
} }
} }
var visibleHeaderIDs = [String]() {
var visibleRange: (start: Int, length: Int)? {
didSet { didSet {
guard oldValue != visibleHeaderIDs else {return} configureVisibleHeaderView(animated: true)
configureVisibleHeaderView(animated: oldValue.count > 0)
} }
} }
@ -50,43 +47,40 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV
} }
func configureVisibleHeaderView(animated: Bool) { func configureVisibleHeaderView(animated: Bool) {
// // no visible header // no visible header
// guard visibleHeaderIDs.count > 0 else { guard let visibleRange = visibleRange else {
// visibleHeaderIndicator.isHidden = true visibleHeaderIndicator.isHidden = true
// return return
// } }
//
// // calculations let topIndexPath = IndexPath(row: visibleRange.start, section: 0)
// guard let minIndex = headings.index(where: {$0.id == visibleHeaderIDs.first}), let bottomIndexPath = IndexPath(row: visibleRange.start + visibleRange.length - 1, section: 0)
// let maxIndex = headings.index(where: {$0.id == visibleHeaderIDs.last}) else {return} let topCellFrame = tableView.rectForRow(at: topIndexPath)
// let topIndexPath = IndexPath(row: minIndex, section: 0) let bottomCellFrame = tableView.rectForRow(at: bottomIndexPath)
// let bottomIndexPath = IndexPath(row: maxIndex, section: 0) let top = topCellFrame.origin.y + topCellFrame.height * 0.1
// let topCellFrame = tableView.rectForRow(at: topIndexPath) let bottom = bottomCellFrame.origin.y + bottomCellFrame.height * 0.9
// let bottomCellFrame = tableView.rectForRow(at: bottomIndexPath)
// let top = topCellFrame.origin.y + topCellFrame.height * 0.1 // indicator frame
// let bottom = bottomCellFrame.origin.y + bottomCellFrame.height * 0.9 visibleHeaderIndicator.isHidden = false
// if animated {
// // indicator frame UIView.animate(withDuration: 0.1, animations: {
// visibleHeaderIndicator.isHidden = false self.visibleHeaderIndicator.frame = CGRect(x: 0, y: top, width: 3, height: bottom - top)
// if animated { })
// UIView.animate(withDuration: 0.1, animations: { } else {
// self.visibleHeaderIndicator.frame = CGRect(x: 0, y: top, width: 3, height: bottom - top) 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
// // tableview scroll switch (topCellVisible, bottomCellVisible) {
// let topCellVisible = tableView.indexPathsForVisibleRows?.contains(topIndexPath) ?? false case (true, false):
// let bottomCellVisible = tableView.indexPathsForVisibleRows?.contains(bottomIndexPath) ?? false tableView.scrollToRow(at: bottomIndexPath, at: .bottom, animated: animated)
// switch (topCellVisible, bottomCellVisible) { case (false, true), (false, false):
// case (true, false): tableView.scrollToRow(at: topIndexPath, at: .top, animated: animated)
// tableView.scrollToRow(at: bottomIndexPath, at: .bottom, animated: animated) default:
// case (false, true), (false, false): return
// tableView.scrollToRow(at: topIndexPath, at: .top, animated: animated) }
// default:
// return
// }
} }
// MARK: - Table view data source // MARK: - Table view data source
@ -113,7 +107,7 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV
default: default:
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = heading.textContent cell.textLabel?.text = heading.textContent
cell.indentationLevel = max(0, (heading.level - headinglevelMin) * 2) cell.indentationLevel = (heading.level - 2) * 2
return cell return cell
} }
} }
@ -121,7 +115,7 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV
// MARK: - Table view delegate // MARK: - Table view delegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.didSelectTOCItem(heading: headings[indexPath.row]) delegate?.didSelectHeading(index: indexPath.row)
tableView.deselectRow(at: indexPath, animated: true) tableView.deselectRow(at: indexPath, animated: true)
} }
@ -145,5 +139,5 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV
} }
protocol TableOfContentsDelegate: class { protocol TableOfContentsDelegate: class {
func didSelectTOCItem(heading: HTMLHeading) func didSelectHeading(index: Int)
} }

View File

@ -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 () { function TableOfContents () {
this.getHeaderElements = function () { this.headings = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
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;
}
}
function getVisibleElementsChecker() { this.getHeadingObjects = function () {
visEle = visEle == undefined ? new VisibleElements() : visEle; var headings = [];
return visEle; for (i = 0; i < this.headings.length; i++) {
} var element = this.headings[i];
var obj = {};
function VisibleElements () { obj.id = element.id;
// return a 2d array [[header(h1/h2/h3), p, ul, div]] obj.index = i;
function getElementGroups() { obj.textContent = element.textContent;
var groups = []; obj.tagName = element.tagName;
var group = [document.getElementsByTagName('h1')[0]]; headings.push(obj);
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);
} }
return headings;
groups.push(group);
return groups;
} }
this.elementGroups = getElementGroups(); this.scrollToView = function (index) {
this.headings[index].scrollIntoView();
this.getVisibleHeaders = function () { }
var groups = this.elementGroups;
this.getVisibleHeadingIndex = function () {
var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight); var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
var visibleHeaders = []; var aboveIndexes = [];
var visibleIndexes = [];
for (i = 0; i < groups.length; i++) { var belowIndexes = [];
var group = groups[i];
var header = group[0]; for (i = 0; i < this.headings.length; i++) {
var groupVisible = false; var element = this.headings[i];
var rect = element.getBoundingClientRect();
for (j = 0; j < group.length; j++) {
var element = group[j]; var isAboveTopBorder = rect.bottom < 0;
var rect = element.getBoundingClientRect(); var isBelowBottomBorder = viewHeight - rect.top < 0;
var isVisible = !(rect.bottom < 0 || rect.top - viewHeight >= 0);
groupVisible = groupVisible || isVisible; if (isAboveTopBorder) {
if (isVisible) { // if found an element visible in group, break and check the next group aboveIndexes.push(i);
visibleHeaders.push(header) } else if (isBelowBottomBorder) {
break; belowIndexes.push(i);
} } else {
} visibleIndexes.push(i);
// 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;
} }
} }
return visibleHeaders;
}
this.getVisibleHeaderIDs = function () {
return this.getVisibleHeaders().map(function(x){ return x.id });
}
}
function startCallBack() { if (aboveIndexes.length > 0) {
function arraysEqual(a, b) { return [aboveIndexes[aboveIndexes.length-1]].concat(visibleIndexes);
if (a === b) return true; } else {
if (a == null || b == null) return false; return visibleIndexes;
if (a.length != b.length) return false;
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
} }
return true;
} }
function callBack(visibleHeaderIDs) { this.startCallBack = function () {
var parameter = visibleHeaderIDs.map(function(x){ return 'header=' + x }).join('&'); var handleScroll = function() {
window.location = 'pagescroll:scrollEnded?' + parameter; var indexes = tableOfContents.getVisibleHeadingIndex();
} if (indexes.length > 0) {
window.location = 'pagescroll:scrollEnded?start=' + indexes[0] + '&length=' + indexes.length;
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;
}
} }
} }
window.onscroll = handleScroll;
handleScroll();
}
this.stopCallBack = function () {
window.onscroll = undefined;
} }
return null;
} }
var tableOfContents = new TableOfContents();