mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-26 13:29:31 -04:00
table of content
This commit is contained in:
parent
fcf6fc46a4
commit
a5636e0b60
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
return headings;
|
||||
}
|
||||
|
||||
function getVisibleElementsChecker() {
|
||||
visEle = visEle == undefined ? new VisibleElements() : visEle;
|
||||
return visEle;
|
||||
this.scrollToView = function (index) {
|
||||
this.headings[index].scrollIntoView();
|
||||
}
|
||||
|
||||
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;
|
||||
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();
|
Loading…
x
Reference in New Issue
Block a user