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
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 {

View File

@ -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) {

View File

@ -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)
}

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 () {
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.headings = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
this.headerElements = this.getHeaderElements();
this.getHeaderObjects = function () {
return this.headerElements.map( elementToObject );
}
this.headerObjects = this.getHeaderObjects();
function elementToObject(element, index) {
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 = index;
obj.index = i;
obj.textContent = element.textContent;
obj.tagName = element.tagName;
return obj;
headings.push(obj);
}
}
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);
return headings;
}
groups.push(group);
return groups;
this.scrollToView = function (index) {
this.headings[index].scrollIntoView();
}
this.elementGroups = getElementGroups();
this.getVisibleHeaders = function () {
var groups = this.elementGroups;
this.getVisibleHeadingIndex = function () {
var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
var visibleHeaders = [];
var aboveIndexes = [];
var visibleIndexes = [];
var belowIndexes = [];
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];
for (i = 0; i < this.headings.length; i++) {
var element = this.headings[i];
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;
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);
}
}
// 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;
if (aboveIndexes.length > 0) {
return [aboveIndexes[aboveIndexes.length-1]].concat(visibleIndexes);
} else {
return visibleIndexes;
}
}
return visibleHeaders;
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.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;
}
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() {
this.stopCallBack = function () {
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;
}
}
}
}
return null;
}
var tableOfContents = new TableOfContents();