Skip to content

Commit

Permalink
Merge pull request #534 from kiwix/filter_library
Browse files Browse the repository at this point in the history
Add filters to kiwix-serve welcome page
  • Loading branch information
kelson42 authored Jun 7, 2021
2 parents 3a4e830 + 1ccafe2 commit 2ef4888
Show file tree
Hide file tree
Showing 11 changed files with 575 additions and 31 deletions.
2 changes: 1 addition & 1 deletion src/server/internalServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const

std::unique_ptr<Response> InternalServer::build_homepage(const RequestContext& request)
{
return ContentResponse::build(*this, RESOURCE::templates::index_html, get_default_data(), "text/html; charset=utf-8");
return ContentResponse::build(*this, RESOURCE::templates::index_html, get_default_data(), "text/html; charset=utf-8", true);
}

std::unique_ptr<Response> InternalServer::handle_meta(const RequestContext& request)
Expand Down
2 changes: 1 addition & 1 deletion src/server/internalServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class InternalServer {
std::string m_server_id;

friend std::unique_ptr<Response> Response::build(const InternalServer& server);
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype);
friend std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage);
friend std::unique_ptr<Response> ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item);
friend std::unique_ptr<Response> Response::build_500(const InternalServer& server, const std::string& msg);

Expand Down
8 changes: 4 additions & 4 deletions src/server/response.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -349,21 +349,21 @@ ContentResponse::ContentResponse(const std::string& root, bool verbose, bool wit
add_header(MHD_HTTP_HEADER_CONTENT_TYPE, m_mimeType);
}

std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype)
std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage)
{
return std::unique_ptr<ContentResponse>(new ContentResponse(
server.m_root,
server.m_verbose.load(),
server.m_withTaskbar,
server.m_withTaskbar && !isHomePage,
server.m_withLibraryButton,
server.m_blockExternalLinks,
content,
mimetype));
}

std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype) {
std::unique_ptr<ContentResponse> ContentResponse::build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype, bool isHomePage) {
auto content = render_template(template_str, data);
return ContentResponse::build(server, content, mimetype);
return ContentResponse::build(server, content, mimetype, isHomePage);
}

ItemResponse::ItemResponse(bool verbose, const zim::Item& item, const std::string& mimetype, const ByteRange& byterange) :
Expand Down
4 changes: 2 additions & 2 deletions src/server/response.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ class Response {
class ContentResponse : public Response {
public:
ContentResponse(const std::string& root, bool verbose, bool withTaskbar, bool withLibraryButton, bool blockExternalLinks, const std::string& content, const std::string& mimetype);
static std::unique_ptr<ContentResponse> build(const InternalServer& server, const std::string& content, const std::string& mimetype);
static std::unique_ptr<ContentResponse> build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype);
static std::unique_ptr<ContentResponse> build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage = false);
static std::unique_ptr<ContentResponse> build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype, bool isHomePage = false);

void set_taskbar(const std::string& bookName, const std::string& bookTitle);

Expand Down
4 changes: 4 additions & 0 deletions static/resources_list.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ skin/jquery-ui/jquery-ui.theme.min.css
skin/jquery-ui/jquery-ui.min.css
skin/caret.png
skin/taskbar.js
skin/langList.js
skin/categoryList.js
skin/iso6391To3.js
skin/isotope.pkgd.min.js
skin/index.js
skin/taskbar.css
skin/block_external.js
Expand Down
19 changes: 19 additions & 0 deletions static/skin/categoryList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// eslint-disable-next-line no-unused-vars
const categoryList = {
"other": "Other",
"gutenberg": "Gutenberg",
"mooc": "Mooc",
"phet": "Phet",
"psiram": "Psiram",
"stack_exchange": "Stack Exchange",
"ted": "Ted",
"vikidia": "Vikidia",
"wikibooks": "Wikibooks",
"wikinews": "Wikinews",
"wikipedia": "Wikipedia",
"wikiquote": "Wikiquote",
"wikisource": "Wikisource",
"wikiversity": "Wikiversity",
"wikivoyage": "Wikivoyage",
"wiktionary": "Wiktionary"
}
187 changes: 169 additions & 18 deletions static/skin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@
start: 0,
count: viewPortToCount()
};
const filterTypes = ['lang', 'category', 'q'];
const bookMap = new Map();
let footer;
let fadeOutDiv;
let iso;
let isFetching = false;
let noResultInjected = false;
let params = new URLSearchParams(window.location.search);
let timer;

function queryUrlBuilder() {
let url = `${root}/catalog/search?`;
url += Object.keys(incrementalLoadingParams).map(key => `${key}=${incrementalLoadingParams[key]}`).join("&");
return url;
params.forEach((value, key) => {url+= value ? `&${key}=${value}` : ''});
return (url);
}

function htmlEncode(str) {
return str.replace(/[\u00A0-\u9999<>\&]/gim, (i) => `&#${i.charCodeAt(0)};`);
}

function viewPortToCount(){
Expand All @@ -21,7 +33,7 @@
return node.querySelector(query).innerHTML;
}

function generateBookHtml(book) {
function generateBookHtml(book, sort = false) {
const link = book.querySelector('link').getAttribute('href');
const title = getInnerHtml(book, 'title');
const description = getInnerHtml(book, 'summary');
Expand All @@ -30,35 +42,143 @@
const articleCount = getInnerHtml(book, 'articleCount');
const mediaCount = getInnerHtml(book, 'mediaCount');

return `<a href='${link}' data-id='${id}'><div class='book'>
<div class='book__background' style="background-image: url('${iconUrl}');">
const linkTag = document.createElement('a');
linkTag.setAttribute('class', 'book');
linkTag.setAttribute('data-id', id);
linkTag.setAttribute('href', link);
if (sort) {
linkTag.setAttribute('data-idx', bookMap[id]);
}
linkTag.innerHTML = `<div class='book__background' style="background-image: url('${iconUrl}');">
<div class='book__title' title='${title}'>${title}</div>
<div class='book__description' title='${description}'>${description}</div>
<div class='book__info'>${articleCount} articles, ${mediaCount} medias</div>
</div>
</div></a>`;
</div>`;
return linkTag;
}

async function loadAndDisplayBooks() {
if (isFetching) return;
isFetching = true;
fetch(queryUrlBuilder()).then(async (resp) => {
function toggleFooter(show=false) {
if (show) {
footer.style.display = 'block';
} else {
footer.style.display = 'none';
fadeOutDiv.style.display = 'block';
}
}

async function loadBooks() {
const loader = document.querySelector('.loader');
loader.style.display = 'block';
return await fetch(queryUrlBuilder()).then(async (resp) => {
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
const books = data.querySelectorAll('entry');
let bookHtml = '';
books.forEach((book) => {bookHtml += generateBookHtml(book)});
document.querySelector('.book__list').innerHTML += bookHtml;
books.forEach((book, idx) => {
bookMap.set(getInnerHtml(book, 'id'), idx);
});
incrementalLoadingParams.start += books.length;
if (books.length < incrementalLoadingParams.count) {
if (parseInt(data.querySelector('totalResults').innerHTML) === bookMap.size) {
incrementalLoadingParams.count = 0;
toggleFooter(true);
} else {
toggleFooter();
}
isFetching = false;
loader.style.display = 'none';
return books;
});
}

async function loadAndDisplayOptions(nodeQuery, query) {
// currently taking an object in place of query, will replace it with query while fetching data from backend later on.
document.querySelector(nodeQuery).innerHTML += Object.keys(query)
.map((option) => {return `<option value='${option}'>${htmlEncode(query[option])}</option>`})
.join('');
}

function checkAndInjectEmptyMessage() {
if (!bookMap.size) {
if (!noResultInjected) {
noResultInjected = true;
iso.remove(document.getElementsByClassName('book__list')[0].getElementsByTagName('a'));
iso.layout();
const spanTag = document.createElement('span');
spanTag.setAttribute('class', 'noResults');
spanTag.innerHTML = `No result. Would you like to <a href="/?lang=">reset filter?</a>`;
document.querySelector('body').append(spanTag);
spanTag.getElementsByTagName('a')[0].onclick = (event) => {
event.preventDefault();
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?lang=`);
resetAndFilter();
filterTypes.forEach(key => {document.getElementsByName(key)[0].value = params.get(key) || ''});
};
}
return true;
} else if (noResultInjected) {
noResultInjected = false;
document.getElementsByClassName('noResults')[0].remove();
}
return false;
}

async function loadAndDisplayBooks(sort = false) {
if (isFetching) return;
isFetching = true;
await loadAndDisplayBooksUnguarded(sort);
isFetching = false;
}

async function loadAndDisplayBooksUnguarded(sort) {
let books = await loadBooks();
if (checkAndInjectEmptyMessage()) {return}
const booksToFilter = new Set();
const booksToDelete = new Set();
iso.arrange({
filter: function (idx, elem) {
const id = elem.getAttribute('data-id');
const retVal = bookMap.has(id);
if (retVal) {
booksToFilter.add(id);
if (sort) {
elem.setAttribute('data-idx', bookMap[id]);
iso.updateSortData(elem);
}
} else {
booksToDelete.add(elem);
}
return retVal;
}
});
books = [...books].filter((book) => {return !booksToFilter.has(getInnerHtml(book, 'id'))});
booksToDelete.forEach(book => {iso.remove(book);});
books.forEach((book) => {iso.insert(generateBookHtml(book, sort))});
}

async function resetAndFilter(filterType = '', filterValue = '') {
isFetching = false;
incrementalLoadingParams.start = 0;
incrementalLoadingParams.count = viewPortToCount();
fadeOutDiv.style.display = 'none';
bookMap.clear();
params = new URLSearchParams(window.location.search);
if (filterType) {
params.set(filterType, filterValue);
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`);
}
await loadAndDisplayBooks(true);
}

window.addEventListener('popstate', async () => {
await resetAndFilter();
filterTypes.forEach(key => {document.getElementsByName(key)[0].value = params.get(key) || ''});
});

async function loadSubset() {
if (incrementalLoadingParams.count && window.innerHeight + window.scrollY >= document.body.offsetHeight) {
loadAndDisplayBooks();
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
if (incrementalLoadingParams.count) {
loadAndDisplayBooks();
}
else {
fadeOutDiv.style.display = 'none';
}
}
}

Expand All @@ -73,6 +193,37 @@
window.addEventListener('scroll', loadSubset);

window.onload = async () => {
loadAndDisplayBooks();
iso = new Isotope( '.book__list', {
itemSelector: '.book',
getSortData:{
weight: function( itemElem ) {
const index = itemElem.getAttribute('data-idx');
return index ? parseInt(index) : Infinity;
}
},
sortBy: 'weight'
});
footer = document.getElementById('kiwixfooter');
fadeOutDiv = document.getElementById('fadeOut');
await loadAndDisplayBooks();
await loadAndDisplayOptions('#languageFilter', langList);
await loadAndDisplayOptions('#categoryFilter', categoryList);
filterTypes.forEach((filter) => {
const filterTag = document.getElementsByName(filter)[0];
filterTag.addEventListener('change', () => {resetAndFilter(filterTag.name, filterTag.value)});
});
params.forEach((value, key) => {document.getElementsByName(key)[0].value = value});
document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()};
if (!window.location.search) {
const browserLang = navigator.language.split('-')[0];
if (browserLang.length === 3) {
document.getElementById('languageFilter').value = browserLang;
langFilter.dispatchEvent(new Event('change'));
} else {
const langFilter = document.getElementById('languageFilter');
langFilter.value = iso6391To3[browserLang];
langFilter.dispatchEvent(new Event('change'));
}
}
}
})();
Loading

0 comments on commit 2ef4888

Please sign in to comment.