mirror of https://github.com/kiwix/libkiwix.git
added filter functionality
This commit is contained in:
parent
3a4e8303a0
commit
bb92f26b60
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) :
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -19,6 +19,9 @@ skin/jquery-ui/jquery-ui.theme.min.css
|
|||
skin/jquery-ui/jquery-ui.min.css
|
||||
skin/caret.png
|
||||
skin/taskbar.js
|
||||
skin/langList.json
|
||||
skin/categoryList.json
|
||||
skin/isotope.pkgd.min.js
|
||||
skin/index.js
|
||||
skin/taskbar.css
|
||||
skin/block_external.js
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"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"
|
||||
}
|
|
@ -1,16 +1,19 @@
|
|||
(function() {
|
||||
const root = $(`link[type='root']`).attr('href');
|
||||
let isFetching = false;
|
||||
let iso;
|
||||
let bookMap = {};
|
||||
const incrementalLoadingParams = {
|
||||
start: 0,
|
||||
count: viewPortToCount()
|
||||
};
|
||||
let isFetching = false;
|
||||
let timer;
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
const filterTypes = ['lang', 'category', 'q'];
|
||||
|
||||
function queryUrlBuilder() {
|
||||
let url = `${root}/catalog/search?`;
|
||||
url += Object.keys(incrementalLoadingParams).map(key => `${key}=${incrementalLoadingParams[key]}`).join("&");
|
||||
return url;
|
||||
return (url + (params.toString() ? `&${params.toString()}` : ''));
|
||||
}
|
||||
|
||||
function viewPortToCount(){
|
||||
|
@ -21,41 +24,109 @@
|
|||
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');
|
||||
const linkTag = document.createElement('a');
|
||||
const id = getInnerHtml(book, 'id');
|
||||
const iconUrl = getInnerHtml(book, 'icon');
|
||||
const articleCount = getInnerHtml(book, 'articleCount');
|
||||
const mediaCount = getInnerHtml(book, 'mediaCount');
|
||||
linkTag.setAttribute('class', 'book');
|
||||
linkTag.setAttribute('data-id', id);
|
||||
linkTag.setAttribute('href', link);
|
||||
if (sort) {
|
||||
linkTag.setAttribute('data-idx', bookMap[id]);
|
||||
}
|
||||
|
||||
return `<a href='${link}' data-id='${id}'><div class='book'>
|
||||
<div class='book__background' style="background-image: url('${iconUrl}');">
|
||||
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) => {
|
||||
async function loadBooks() {
|
||||
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[getInnerHtml(book, 'id')] = idx;
|
||||
});
|
||||
incrementalLoadingParams.start += books.length;
|
||||
if (books.length < incrementalLoadingParams.count) {
|
||||
incrementalLoadingParams.count = 0;
|
||||
}
|
||||
isFetching = false;
|
||||
return books;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAndDisplayOptions(nodeQuery, query) {
|
||||
// currently taking an array in place of query, will replace it with query while fetching data from backend later on.
|
||||
await fetch(query)
|
||||
.then(async (resp) => {
|
||||
const data = await resp.json();
|
||||
Object.keys(data).forEach((option) => {
|
||||
document.querySelector(nodeQuery).innerHTML += `<option value='${option}'>${data[option]}</option>`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAndDisplayBooks(sort = false) {
|
||||
if (isFetching) return;
|
||||
isFetching = true;
|
||||
let books = await loadBooks();
|
||||
const booksToFilter = new Set();
|
||||
const booksToDelete = new Set();
|
||||
iso.arrange({
|
||||
filter: function (idx, elem) {
|
||||
const id = elem.getAttribute('data-id');
|
||||
const retVal = bookMap.hasOwnProperty(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))});
|
||||
isFetching = false;
|
||||
}
|
||||
|
||||
async function filterBooks(filterType, filterValue) {
|
||||
isFetching = false;
|
||||
incrementalLoadingParams.start = 0;
|
||||
incrementalLoadingParams.count = viewPortToCount();
|
||||
bookMap = {};
|
||||
params = new URLSearchParams(window.location.search);
|
||||
if (!filterValue) {
|
||||
params.delete(filterType);
|
||||
} else {
|
||||
params.set(filterType, filterValue);
|
||||
}
|
||||
window.history.pushState({}, null, `${window.location.href.split('?')[0]}?${params.toString()}`);
|
||||
await loadAndDisplayBooks(true);
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', async () => {
|
||||
bookMap = {};
|
||||
isFetching = false;
|
||||
incrementalLoadingParams.start = 0;
|
||||
incrementalLoadingParams.count = viewPortToCount();
|
||||
params = new URLSearchParams(window.location.search);
|
||||
filterTypes.forEach(key => {document.getElementsByName(key)[0].value = params.get(key) || ''});
|
||||
loadAndDisplayBooks(true);
|
||||
});
|
||||
|
||||
async function loadSubset() {
|
||||
if (incrementalLoadingParams.count && window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
||||
loadAndDisplayBooks();
|
||||
|
@ -73,6 +144,26 @@
|
|||
window.addEventListener('scroll', loadSubset);
|
||||
|
||||
window.onload = async () => {
|
||||
iso = new Isotope( '.book__list', {
|
||||
itemSelector: '.book',
|
||||
getSortData:{
|
||||
weight: function( itemElem ) {
|
||||
const index = itemElem.getAttribute('data-idx');
|
||||
return index ? parseInt(index) : Infinity;
|
||||
}
|
||||
},
|
||||
sortBy: 'weight'
|
||||
});
|
||||
loadAndDisplayBooks();
|
||||
loadAndDisplayOptions('#languageFilter', `${root}/skin/langList.json`);
|
||||
loadAndDisplayOptions('#categoryFilter', `${root}/skin/categoryList.json`);
|
||||
for (const key of params.keys()) {
|
||||
document.getElementsByName(key)[0].value = params.get(key);
|
||||
}
|
||||
filterTypes.forEach((filter) => {
|
||||
const filterTag = document.getElementsByName(filter)[0];
|
||||
filterTag.addEventListener('change', () => {filterBooks(filterTag.name, filterTag.value)});
|
||||
});
|
||||
document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()}
|
||||
}
|
||||
})();
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,124 @@
|
|||
{
|
||||
"aar": "Afar",
|
||||
"afr": "Afrikaans",
|
||||
"aka": "Akan",
|
||||
"amh": "Amharic",
|
||||
"ara": "Arabic",
|
||||
"asm": "Assamese",
|
||||
"aze": "Azerbaijani",
|
||||
"bak": "Bashkir",
|
||||
"bel": "Belarusian",
|
||||
"bul": "Bulgarian",
|
||||
"bam": "Bambara",
|
||||
"ben": "Bengali",
|
||||
"bod": "Tibetan",
|
||||
"bre": "Breton",
|
||||
"bos": "Bosnian",
|
||||
"cat": "Catalan",
|
||||
"che": "Chechen",
|
||||
"cos": "Corsican",
|
||||
"ces": "Czech",
|
||||
"chv": "Chuvash",
|
||||
"cym": "Welsh",
|
||||
"dan": "Danish",
|
||||
"deu": "German",
|
||||
"dzo": "Dzongkha",
|
||||
"ewe": "Ewe",
|
||||
"eng": "English",
|
||||
"spa": "Spanish",
|
||||
"est": "Estonian",
|
||||
"eus": "Basque",
|
||||
"fas": "Persian",
|
||||
"ful": "Fulah",
|
||||
"fin": "Finnish",
|
||||
"fao": "Faroese",
|
||||
"fra": "French",
|
||||
"gle": "Irish",
|
||||
"glg": "Galician",
|
||||
"grn": "Guarani",
|
||||
"guj": "Gujarati",
|
||||
"glv": "Manx",
|
||||
"hau": "Hausa",
|
||||
"heb": "Hebrew",
|
||||
"hin": "Hindi",
|
||||
"hrv": "Croatian",
|
||||
"hun": "Hungarian",
|
||||
"hye": "Armenian",
|
||||
"ind": "Indonesian",
|
||||
"ibo": "Igbo",
|
||||
"isl": "Icelandic",
|
||||
"ita": "Italian",
|
||||
"iku": "Inuktitut",
|
||||
"jpn": "Japanese",
|
||||
"jav": "Javanese",
|
||||
"kat": "Georgian",
|
||||
"kik": "Kikuyu",
|
||||
"kaz": "Kazakh",
|
||||
"khm": "Khmer",
|
||||
"kan": "Kannada",
|
||||
"kor": "Korean",
|
||||
"kas": "Kashmiri",
|
||||
"kur": "Kurdish",
|
||||
"cor": "Cornish",
|
||||
"kir": "Kirghiz",
|
||||
"ltz": "Luxembourgish",
|
||||
"lug": "Ganda",
|
||||
"lin": "Lingala",
|
||||
"lao": "Lao",
|
||||
"lit": "Lithuanian",
|
||||
"lav": "Latvian",
|
||||
"mlg": "Malagasy",
|
||||
"mri": "Maori",
|
||||
"mkd": "Macedonian",
|
||||
"mal": "Malayalam",
|
||||
"mon": "Mongolian",
|
||||
"mar": "Marathi",
|
||||
"mlt": "Maltese",
|
||||
"mya": "Burmese",
|
||||
"nld": "Dutch",
|
||||
"nya": "Nyanja",
|
||||
"orm": "Oromo",
|
||||
"pol": "Polish",
|
||||
"por": "Portuguese",
|
||||
"que": "Quechua",
|
||||
"roh": "Romansh",
|
||||
"run": "Rundi",
|
||||
"ron": "Romanian",
|
||||
"rus": "Russian",
|
||||
"kin": "Kinyarwanda",
|
||||
"san": "Sanskrit",
|
||||
"snd": "Sindhi",
|
||||
"sag": "Sango",
|
||||
"sin": "Sinhala",
|
||||
"slk": "Slovak",
|
||||
"slv": "Slovenian",
|
||||
"sna": "Shona",
|
||||
"som": "Somali",
|
||||
"sqi": "Albanian",
|
||||
"srp": "Serbian",
|
||||
"ssw": "Swati",
|
||||
"swe": "Swedish",
|
||||
"tam": "Tamil",
|
||||
"tel": "Telugu",
|
||||
"tgk": "Tajik",
|
||||
"tha": "Thai",
|
||||
"tir": "Tigrinya",
|
||||
"tuk": "Turkmen",
|
||||
"fil": "Filipino",
|
||||
"tsn": "Tswana",
|
||||
"tur": "Turkish",
|
||||
"tso": "Tsonga",
|
||||
"tat": "Tatar",
|
||||
"uig": "Uighur",
|
||||
"ukr": "Ukrainian",
|
||||
"urd": "Urdu",
|
||||
"uzb": "Uzbek",
|
||||
"ven": "Venda",
|
||||
"vie": "Vietnamese",
|
||||
"wln": "Walloon",
|
||||
"wol": "Wolof",
|
||||
"xho": "Xhosa",
|
||||
"yor": "Yoruba",
|
||||
"zho": "Chinese",
|
||||
"zul": "Zulu"
|
||||
}
|
|
@ -49,7 +49,27 @@
|
|||
font-family: sans-serif;
|
||||
font-size: 13px;
|
||||
background-color: #f1f1f1;
|
||||
box-shadow: 2px 2px 5px 0px #ccc;
|
||||
box-shadow: 2px 2px 5px 0 #ccc;
|
||||
}
|
||||
#kiwixfooter {
|
||||
text-align: center;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.kiwixHomeNavbar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.kiwixFilter {
|
||||
margin: 8px 10px;
|
||||
}
|
||||
.kiwixSearchForm {
|
||||
margin: 8px 10px;
|
||||
float: right;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.kiwixHomeBody {
|
||||
padding: 0 125px;
|
||||
}
|
||||
}
|
||||
.book:hover {
|
||||
background-color: #f9f9f9;
|
||||
|
@ -70,7 +90,7 @@
|
|||
line-height: 1em;
|
||||
}
|
||||
.book__description {
|
||||
padding: 5px 55px 5px 0px;
|
||||
padding: 5px 55px 5px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
@ -83,14 +103,32 @@
|
|||
font-size: 13px;
|
||||
line-height: 1em;
|
||||
}
|
||||
a:link {
|
||||
text-decoration: none;
|
||||
}
|
||||
a:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript" src="{{root}}/skin/index.js" async></script>
|
||||
<script src="{{root}}/skin/isotope.pkgd.min.js" defer></script>
|
||||
<script type="text/javascript" src="{{root}}/skin/index.js" defer></script>
|
||||
</head>
|
||||
<body class="kiwix">
|
||||
<div class="kiwix">
|
||||
<div class='kiwixHomeNavbar'>
|
||||
<select name="lang" id="languageFilter" class='kiwixFilter'>
|
||||
<option value="" selected>All languages</option>
|
||||
</select>
|
||||
<select name="category" id="categoryFilter" class='kiwixFilter'>
|
||||
<option value="" selected>ALl categories</option>
|
||||
</select>
|
||||
<form id='kiwixSearchForm' class='kiwixSearchForm'>
|
||||
<input type="text" name="q" id="searchFilter" class='kiwixSearch'>
|
||||
<input type="submit" value="Submit"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="kiwixHomeBody">
|
||||
<div class="book__list"></div>
|
||||
</div>
|
||||
|
||||
<div id="kiwixfooter">Powered by <a href="https://kiwix.org">Kiwix</a></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in New Issue