js toc visible detection

This commit is contained in:
Chris Li 2016-10-18 14:32:10 -04:00
parent ed9c927189
commit 795d129234
7 changed files with 174 additions and 5 deletions

View File

@ -27,8 +27,16 @@ class JS {
return webView.stringByEvaluatingJavaScriptFromString("document.title")
}
class func startTOCCallBack(webView: UIWebView) {
webView.stringByEvaluatingJavaScriptFromString("startCallBack()")
}
class func stopTOCCallBack(webView: UIWebView) {
webView.stringByEvaluatingJavaScriptFromString("stopCallBack()")
}
class func getTableOfContents(webView: UIWebView) -> [HTMLHeading] {
let jString = "(new TableOfContents()).headerObjects;"
let jString = "getTableOfContents().headerObjects;"
guard let elements = webView.context.evaluateScript(jString).toArray() as? [[String: String]] else {return [HTMLHeading]()}
var headings = [HTMLHeading]()
for element in elements {

View File

@ -18,6 +18,12 @@ extension MainController: UIWebViewDelegate, SFSafariViewControllerDelegate, LPT
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
guard let url = request.URL else {return false}
guard url.scheme?.caseInsensitiveCompare("pagescroll") != .OrderedSame else {
let components = NSURLComponents(string: url.absoluteString!)
let ids = components?.queryItems?.filter({$0.name == "header"}).flatMap({$0.value}) ?? [String]()
tableOfContentsController?.visibleHeaderIDs = ids
return false
}
guard url.isKiwixURL else {
let controller = SFSafariViewController(URL: url)
controller.delegate = self

View File

@ -120,6 +120,8 @@ extension MainController {
self.view.layoutIfNeeded()
self.dimView.alpha = 0.5
}) { (completed) in }
JS.startTOCCallBack(webView)
}
func hideTableOfContentsController() {
@ -133,6 +135,8 @@ extension MainController {
self.dimView.hidden = true
self.tocVisiualEffectView.hidden = true
}
JS.stopTOCCallBack(webView)
}
func configureTOCViewConstraints() {

View File

@ -12,6 +12,7 @@ import DZNEmptyDataSet
class TableOfContentsController: UIViewController, UITableViewDelegate, UITableViewDataSource, DZNEmptyDataSetSource, DZNEmptyDataSetDelegate {
@IBOutlet weak var tableView: UITableView!
private let visibleHeaderIndicator = UIView()
weak var delegate: TableOfContentsDelegate?
private var headinglevelMin = 0
@ -19,9 +20,16 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV
didSet {
configurePreferredContentSize()
headinglevelMin = max(2, headings.map({$0.level}).minElement() ?? 0)
visibleHeaderIDs.removeAll()
tableView.reloadData()
}
}
var visibleHeaderIDs = [String]() {
didSet {
guard oldValue != visibleHeaderIDs else {return}
configureVisibleHeaderView(animated: oldValue.count > 0)
}
}
override func viewDidLoad() {
super.viewDidLoad()
@ -30,6 +38,7 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV
tableView.emptyDataSetSource = self
tableView.emptyDataSetDelegate = self
tableView.tableFooterView = UIView()
visibleHeaderIndicator.backgroundColor = UIColor.redColor()
}
func configurePreferredContentSize() {
@ -37,7 +46,47 @@ class TableOfContentsController: UIViewController, UITableViewDelegate, UITableV
let width = traitCollection.horizontalSizeClass == .Regular ? 300 : (UIScreen.mainScreen().bounds.width)
preferredContentSize = CGSizeMake(width, count == 0 ? 350 : min(CGFloat(count) * 44.0, UIScreen.mainScreen().bounds.height * 0.8))
}
func configureVisibleHeaderView(animated animated: Bool) {
// no visible header
guard visibleHeaderIDs.count > 0 else {
visibleHeaderIndicator.removeFromSuperview()
return
}
// calculations
guard let minIndex = headings.indexOf({$0.id == visibleHeaderIDs.first}),
let maxIndex = headings.indexOf({$0.id == visibleHeaderIDs.last}) else {return}
let topIndexPath = NSIndexPath(forRow: minIndex, inSection: 0)
let bottomIndexPath = NSIndexPath(forRow: maxIndex, inSection: 0)
let topCell = tableView(tableView, cellForRowAtIndexPath: topIndexPath)
let bottomCell = tableView(tableView, cellForRowAtIndexPath: bottomIndexPath)
let top = topCell.frame.origin.y + topCell.frame.height * 0.1
let bottom = bottomCell.frame.origin.y + bottomCell.frame.height * 0.9
// indicator frame
if !tableView.subviews.contains(visibleHeaderIndicator) {tableView.addSubview(visibleHeaderIndicator)}
if animated {
UIView.animateWithDuration(0.1, animations: {
self.visibleHeaderIndicator.frame = CGRectMake(0, top, 3, bottom - top)
})
} else {
visibleHeaderIndicator.frame = CGRectMake(0, top, 3, 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.scrollToRowAtIndexPath(bottomIndexPath, atScrollPosition: .Bottom, animated: animated)
case (false, true), (false, false):
tableView.scrollToRowAtIndexPath(topIndexPath, atScrollPosition: .Top, animated: animated)
default:
return
}
}
// MARK: - Table view data source
func numberOfSectionsInTableView(tableView: UITableView) -> Int {

View File

@ -49,7 +49,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>1.8.1791</string>
<string>1.8.1832</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.8.1791</string>
<string>1.8.1832</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionMainStoryboard</key>

View File

@ -1,3 +1,12 @@
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];
@ -17,7 +26,7 @@ function TableOfContents () {
this.headerElements = this.getHeaderElements();
this.getHeaderObjects = function () {
return this.getHeaderElements().map( elementToObject );
return this.headerElements.map( elementToObject );
}
this.headerObjects = this.getHeaderObjects();
@ -30,3 +39,96 @@ function TableOfContents () {
return 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);
}
groups.push(group);
return groups;
}
this.elementGroups = getElementGroups();
this.getVisibleHeaders = function () {
var groups = this.elementGroups;
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;
}
}
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;
}
return true;
}
visibleHeaderIDs = getVisibleElementsChecker().getVisibleHeaderIDs();
window.onscroll = function() {
var newVisibleHeaderIDs = getVisibleElementsChecker().getVisibleHeaderIDs();
if (!arraysEqual(visibleHeaderIDs, newVisibleHeaderIDs)) {
visibleHeaderIDs = newVisibleHeaderIDs;
console.log(visibleHeaderIDs);
var parameter = visibleHeaderIDs.map(function(x){ return 'header=' + x }).join('&');
window.location = 'pagescroll:scrollEnded?' + parameter;
}
};
}
function stopCallBack() {
window.onscroll = undefined;
}