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

Introduce Table of Content Without Intense JS Invasion #1237

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

ShaopengLin
Copy link
Collaborator

@ShaopengLin ShaopengLin commented Nov 7, 2024

Alternative to #1201. FIx #42

Changes:

  • Uses QTreeWidget to handle TOC display
  • The TOC now resides in the sidebar along with the Content Manager Side and ReadingList Bar.
  • Styling is slightly different since QTreeWidget doesn't perform the same way as HTML and JS.

Benefits:

  • More extensible when future features come
  • Easier handling inside Qt for styling and parsing.

Loses:

  • The TOC requesting is asynchronous and requires some fine-tuned race checking.
  • Slow due to extra parsing and passing around of the TOC.

@veloman-yunkan
Copy link
Collaborator

Bug report - when the TOC is not empty the attempt to open the TOC sidebar results in the latter growing wider and wider indefinitely.

@ShaopengLin
Copy link
Collaborator Author

@veloman-yunkan I can't seem to replicate this. Is this happening on any zim?

@veloman-yunkan
Copy link
Collaborator

@veloman-yunkan I can't seem to replicate this. Is this happening on any zim?

It happens on ray_charles.zim for sure

@veloman-yunkan
Copy link
Collaborator

Screencast.from.11-07-2024.08.22.37.PM.webm

@ShaopengLin
Copy link
Collaborator Author

@veloman-yunkan Would like to confirm if you are on Qt6? I have tested Qt5 and Qt6, where Qt6 seems to be the version that this bug happens

Copy link
Collaborator

@veloman-yunkan veloman-yunkan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the initial feedback from a quick and very superficial review

Comment on lines +10 to +24
QWebEngineScript getScript(QString filename,
QWebEngineScript::InjectionPoint point = QWebEngineScript::DocumentReady)
{
QWebEngineScript script;
script.setInjectionPoint(point);
script.setWorldId(QWebEngineScript::UserWorld);

QFile scriptFile(filename);
scriptFile.open(QIODevice::ReadOnly);
script.setSourceCode(scriptFile.readAll());
return script;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hide in unnamed namespace

src/webview.cpp Outdated
Comment on lines 110 to 136
connect(tabbar, &TabBar::currentTitleChanged, this,
[=](){
const auto tableObject = m_tableOfContent.object();
const auto currentUrl = url().url(QUrl::RemoveFragment);
const auto tableValid = tableObject["url"].toString() == currentUrl;

/* When table invalid, then we are loading and the emit will be
handled by KiwixWebChannelObject::tableOfContentChanged.
*/
if (tabbar->currentWebView() == this && tableValid)
emit tableOfContentChanged(m_tableOfContent);
});
connect(kiwixChannelObj, &KiwixWebChannelObject::tableOfContentChanged, this,
[=](const QString& tableJson){
m_tableOfContent = QJsonDocument::fromJson(tableJson.toUtf8());
if (tabbar->currentWebView() == this)
emit tableOfContentChanged(m_tableOfContent);
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better use named slots instead of anonymous lambdas

Comment on lines +10 to +70
function recurseChild(elem, recurseData)
{
if (elem !== "undefined")
{
if(elem.nodeName.match(/^H\d+$/) && elem.textContent)
{
var headerText = elem.textContent.trim();
var prevLevel = recurseData.level;
var level = elem.nodeName.substr(1);
var anchor = "kiwix-toc-" + recurseData.count;
var anchorLink = window.location.href.replace(location.hash,"") + '#' + anchor;
recurseData.count += 1;

var anchorElem = document.createElement("a");
anchorElem.id = anchor;

/* Wrap header content with something we can reference. */
elem.insertAdjacentElement("afterbegin", anchorElem);

if (level < prevLevel)
{
/* Complete current element and the parent element.*/
recurseData.toc += ']}]}, ';
}
else if (level == prevLevel)
{
/* Complete current element*/
recurseData.toc += ']}, ';
}

recurseData.level = parseInt(level);
recurseData.levelSet.add(parseInt(level));
recurseData.toc += '{"text" : "' + headerText.replace(/"/g, '\\"') + '", "anchor": "' + anchorLink + '", ' + '"child" : [';
}

var c = elem.children;
for (var i = 0; i < c.length; i++)
recurseChild(c[i], recurseData);
}
}

function tocJSON()
{
/* level used to track current header level.
toc used to store constructed list.
count used to uniquely identify each list item in toc.
levelSet used to retrieve the levels disregarding header level value.
*/
var recurseData = { level: 0, toc: '{ "url" : "' + window.location.href.replace(location.hash,"") + '", "table" : [', count: 0, levelSet: new Set};
recurseChild(document.body, recurseData);

var levelArray = Array.from(recurseData.levelSet).sort();
levelArray.sort(function(a, b){return a - b});
var level = levelArray.indexOf(recurseData.level) + 1;

/* End un-closed lists */
if (level)
recurseData.toc += (new Array(level + 1)).join(']}');
recurseData.toc += ']}';
return recurseData.toc;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that instead of constructing the final JSON string it is better to construct a recursive JS object and then call JSON.stringify() on it.

var prevLevel = recurseData.level;
var level = elem.nodeName.substr(1);
var anchor = "kiwix-toc-" + recurseData.count;
var anchorLink = window.location.href.replace(location.hash,"") + '#' + anchor;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it doesn't make sense to include the full URL with every link. The URL of the page can be provided only once at the root level of the returned JSON object while each TOC entry will include only the anchor.

Comment on lines 75 to 80
kiwixObj.navigationRequested.connect(function(anchor) {
var anchorIndex = anchor.lastIndexOf('#');
var anchorPageUrl = anchor.substr(0, anchorIndex);
if (window.location.href.replace(location.hash,"") === anchorPageUrl)
window.location.href = anchor;
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is about the full commit - can you elaborate on the scenario that is problematic if the navigation is implemented in C++? Is your solution fully devoid of similar problems or they are just made less embarrassing for the UX?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I thought I replaced this commit already... I previously forgot the setUrl method and was using this method. Another victim from rebasing.

@veloman-yunkan
Copy link
Collaborator

@veloman-yunkan Would like to confirm if you are on Qt6? I have tested Qt5 and Qt6, where Qt6 seems to be the version that this bug happens

No, I am a qtsaur. I don't use the most recent version.

@veloman-yunkan
Copy link
Collaborator

  • Slow due to extra parsing and passing around of the TOC.

Is this assessment based on your understanding of the implementation or a result of some measurement?

@ShaopengLin
Copy link
Collaborator Author

ShaopengLin commented Nov 7, 2024

Is this assessment based on your understanding of the implementation or a result of some measurement?

@veloman-yunkan It is more like my own understanding and observation from other Stackoverflowers:

  1. QTreeWidget have seen performance issues regarding larger data amounts: 1, 2
  2. In Javascript, instead of creating JSON and doing another round of parsing, we can just create a <ul> <li> list on the first round and immediately append this to the sidebar.
  3. In Qt, everytime we go to a new page, the TOC needs to reload. While If we display using Javascript, the TOC is already in the DOM and no need to reload.

@ShaopengLin
Copy link
Collaborator Author

Bug report - when the TOC is not empty the attempt to open the TOC sidebar results in the latter growing wider and wider indefinitely.

@veloman-yunkan Should be fixed now. Somehow under the same settings, QListWidget in ReadingListBar doesn't have this problem. This might be due to either styling or underlying widget differences.

Setup for TOC Javascript/CSS Injection
Add channel to communicate between web page and Qt
Re-enabled ToggleTOCAction to display the bar.
Only one should be in checked state.
Signal emits the collected header JSON. Parsing delegated to JS due to better DOM manipulations.
Recursively load the header JSON.
Upon signaling, use JS to navigate, as helps avoid wrong url setting given the asynchronism.
Elided item text is expanded here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement table of content
2 participants