Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Record TOC navigations in history (#1248) #1349

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 111 additions & 8 deletions resources/js/headerAnchor.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,114 @@ function getHeadersJSONStr()
return JSON.stringify(headerInfo);
}

new QWebChannel(qt.webChannelTransport, function(channel) {
var kiwixObj = channel.objects.kiwixChannelObj;
kiwixObj.sendHeadersJSONStr(getHeadersJSONStr());
kiwixObj.navigationRequested.connect(function(url, anchor) {
if (window.location.href.replace(location.hash,"") == url)
document.getElementById(anchor).scrollIntoView();
});
});
// Track the current anchor for history state
let currentAnchor = null;
let isNavigating = false;

// Function to scroll to an anchor with animation
function scrollToAnchor(anchor, updateHistory = false) {
if (!anchor || typeof anchor !== 'string') {
console.error("Invalid anchor:", anchor);
return false;
}

if (isNavigating || anchor === currentAnchor) {
console.log("Already navigating to or at anchor: " + anchor);
return true;
}

try {
isNavigating = true;
const element = document.getElementById(anchor);
if (element) {
setTimeout(() => {
element.scrollIntoView({behavior: 'smooth'});
currentAnchor = anchor;

// Update the URL in history if requested
if (updateHistory && window.history && window.history.pushState) {
try {
const baseUrl = window.location.href.replace(location.hash, "");
window.history.pushState({ anchor: anchor }, "", baseUrl + "#" + anchor);
} catch (historyError) {
console.error("Error updating history:", historyError);
}
}

// Reset navigation flag after a short delay
setTimeout(() => {
isNavigating = false;
}, 100);
}, 10);

return true;
}
console.error("Anchor not found: " + anchor);
isNavigating = false;
return false;
} catch (error) {
console.error("Error scrolling to anchor:", error);
isNavigating = false;
return false;
}
}

function initializeWebChannel() {
try {
if (typeof qt === 'undefined' || typeof qt.webChannelTransport === 'undefined') {
console.error("Qt WebChannel not available");
return;
}

new QWebChannel(qt.webChannelTransport, function(channel) {
if (!channel || !channel.objects || !channel.objects.kiwixChannelObj) {
console.error("Kiwix channel object not available");
return;
}

var kiwixObj = channel.objects.kiwixChannelObj;

try {
kiwixObj.sendHeadersJSONStr(getHeadersJSONStr());
} catch (e) {
console.error("Error sending headers:", e);
}

kiwixObj.navigationRequested.connect(function(url, anchor) {
if (isNavigating || anchor === currentAnchor) {
return;
}

if (window.location.href.replace(location.hash, "") == url) {
scrollToAnchor(anchor, false);
}
});

window.addEventListener('popstate', function(event) {
if (isNavigating) {
return;
}

// Handle navigation from history
if (location.hash) {
const anchor = location.hash.substring(1);
if (anchor !== currentAnchor) {
scrollToAnchor(anchor, false);
}
} else if (event.state && event.state.anchor) {
if (event.state.anchor !== currentAnchor) {
scrollToAnchor(event.state.anchor, false);
}
}
});
});
} catch (error) {
console.error("Error initializing web channel:", error);
}
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeWebChannel);
} else {
initializeWebChannel();
}
4 changes: 3 additions & 1 deletion src/kiwixwebchannelobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ class KiwixWebChannelObject : public QObject
explicit KiwixWebChannelObject(QObject *parent = nullptr) : QObject(parent) {};

Q_INVOKABLE void sendHeadersJSONStr(const QString& headersJSONStr) { emit headersChanged(headersJSONStr); };
Q_INVOKABLE void sendConsoleMessage(const QString& message) { emit consoleMessageReceived(message); };

signals:
void headersChanged(const QString& headersJSONStr);
void navigationRequested(const QString& url, const QString& anchor);
void consoleMessageReceived(const QString& message);
};

#endif // KIWIXWEBCHANNELOBJECT_H
#endif // KIWIXWEBCHANNELOBJECT_H
70 changes: 68 additions & 2 deletions src/tableofcontentbar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "kiwixapp.h"
#include <QJsonObject>
#include <QTreeWidgetItem>
#include <QTimer>

TableOfContentBar::TableOfContentBar(QWidget *parent) :
QFrame(parent),
Expand All @@ -24,6 +25,10 @@ TableOfContentBar::TableOfContentBar(QWidget *parent) :
ui->tree->setItemsExpandable(false);
connect(ui->tree, &QTreeWidget::itemClicked, this, &TableOfContentBar::onTreeItemActivated);
connect(ui->tree, &QTreeWidget::itemActivated, this, &TableOfContentBar::onTreeItemActivated);

// Setup debounce timer
m_clickDebounceTimer.setSingleShot(true);
m_clickDebounceTimer.setInterval(300); // 300ms debounce
}

TableOfContentBar::~TableOfContentBar()
Expand All @@ -33,7 +38,37 @@ TableOfContentBar::~TableOfContentBar()

void TableOfContentBar::onTreeItemActivated(QTreeWidgetItem *item)
{
emit navigationRequested(m_url, item->data(0, Qt::UserRole).toString());
//Safety check
if (!item) {
return;
}

// Get the anchor from the item
QVariant anchorVariant = item->data(0, Qt::UserRole);
if (!anchorVariant.isValid()) {
return;
}

QString anchor = anchorVariant.toString();
if (anchor.isEmpty() || m_url.isEmpty()) {
return;
}

if (m_isNavigating || (anchor == m_lastAnchor && m_clickDebounceTimer.isActive())) {
return;
}

m_isNavigating = true;
m_lastAnchor = anchor;
m_clickDebounceTimer.start();

QTimer::singleShot(10, this, [this, anchor]() {
emit navigationRequested(m_url, anchor);

QTimer::singleShot(300, this, [this]() {
m_isNavigating = false;
});
});
}

namespace
Expand Down Expand Up @@ -93,9 +128,40 @@ void TableOfContentBar::setupTree(const QJsonObject& headers)
const auto currentUrl = webView->url().url(QUrl::RemoveFragment);
if (headerUrl != currentUrl)
return;

m_url = headerUrl;
ui->tree->clear();
QJsonArray headerArr = headers["headers"].toArray();
createSubTree(ui->tree->invisibleRootItem(), "", headerArr);

// Update selection based on current URL fragment
updateSelectionFromFragment(webView->url().fragment());
}

void TableOfContentBar::updateSelectionFromFragment(const QString& fragment)
{
if (fragment.isEmpty() || !ui || !ui->tree) {
return;
}

// Find the item with the matching anchor
QTreeWidgetItemIterator it(ui->tree);
while (*it) {
QVariant anchorVariant = (*it)->data(0, Qt::UserRole);
if (!anchorVariant.isValid()) {
++it;
continue;
}

QString anchor = anchorVariant.toString();
if (anchor == fragment) {
// Select the item without triggering navigation
ui->tree->blockSignals(true);
ui->tree->setCurrentItem(*it);
ui->tree->scrollToItem(*it);
ui->tree->blockSignals(false);
break;
}
++it;
}
}
7 changes: 6 additions & 1 deletion src/tableofcontentbar.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define TABLEOFCONTENTBAR_H

#include <QFrame>
#include <QTimer>

namespace Ui {
class tableofcontentbar;
Expand All @@ -20,13 +21,17 @@ class TableOfContentBar : public QFrame
public slots:
void setupTree(const QJsonObject& headers);
void onTreeItemActivated(QTreeWidgetItem* item);
void updateSelectionFromFragment(const QString& fragment);

signals:
void navigationRequested(const QString& url, const QString& anchor);

private:
Ui::tableofcontentbar *ui;
QString m_url;
QTimer m_clickDebounceTimer;
bool m_isNavigating = false;
QString m_lastAnchor;
};

#endif // TABLEOFCONTENTBAR_H
#endif // TABLEOFCONTENTBAR_H
Loading