From bb92f26b60684c6daa0ccbe681fd4ba5a9c8a650 Mon Sep 17 00:00:00 2001 From: Manan Jethwani Date: Tue, 18 May 2021 17:57:22 +0530 Subject: [PATCH] added filter functionality --- src/server/internalServer.cpp | 2 +- src/server/internalServer.h | 2 +- src/server/response.cpp | 8 +- src/server/response.h | 4 +- static/resources_list.txt | 3 + static/skin/categoryList.json | 18 +++++ static/skin/index.js | 125 +++++++++++++++++++++++++++----- static/skin/isotope.pkgd.min.js | 12 +++ static/skin/langList.json | 124 +++++++++++++++++++++++++++++++ static/templates/index.html | 48 ++++++++++-- 10 files changed, 316 insertions(+), 30 deletions(-) create mode 100644 static/skin/categoryList.json create mode 100644 static/skin/isotope.pkgd.min.js create mode 100644 static/skin/langList.json diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp index 8b9ae36ff..4d344edc0 100644 --- a/src/server/internalServer.cpp +++ b/src/server/internalServer.cpp @@ -304,7 +304,7 @@ InternalServer::get_matching_if_none_match_etag(const RequestContext& r) const std::unique_ptr 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 InternalServer::handle_meta(const RequestContext& request) diff --git a/src/server/internalServer.h b/src/server/internalServer.h index 0106014c6..c37c736f2 100644 --- a/src/server/internalServer.h +++ b/src/server/internalServer.h @@ -106,7 +106,7 @@ class InternalServer { std::string m_server_id; friend std::unique_ptr Response::build(const InternalServer& server); - friend std::unique_ptr ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype); + friend std::unique_ptr ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage); friend std::unique_ptr ItemResponse::build(const InternalServer& server, const RequestContext& request, const zim::Item& item); friend std::unique_ptr Response::build_500(const InternalServer& server, const std::string& msg); diff --git a/src/server/response.cpp b/src/server/response.cpp index b1e0ecf35..3ebec43d2 100644 --- a/src/server/response.cpp +++ b/src/server/response.cpp @@ -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::build(const InternalServer& server, const std::string& content, const std::string& mimetype) +std::unique_ptr ContentResponse::build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage) { return std::unique_ptr(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::build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype) { +std::unique_ptr 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) : diff --git a/src/server/response.h b/src/server/response.h index 76bb82f7a..8d113ad7f 100644 --- a/src/server/response.h +++ b/src/server/response.h @@ -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 build(const InternalServer& server, const std::string& content, const std::string& mimetype); - static std::unique_ptr build(const InternalServer& server, const std::string& template_str, kainjow::mustache::data data, const std::string& mimetype); + static std::unique_ptr build(const InternalServer& server, const std::string& content, const std::string& mimetype, bool isHomePage = false); + static std::unique_ptr 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); diff --git a/static/resources_list.txt b/static/resources_list.txt index 6c0570442..fb45dce55 100644 --- a/static/resources_list.txt +++ b/static/resources_list.txt @@ -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 diff --git a/static/skin/categoryList.json b/static/skin/categoryList.json new file mode 100644 index 000000000..cd1f3a7f6 --- /dev/null +++ b/static/skin/categoryList.json @@ -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" +} \ No newline at end of file diff --git a/static/skin/index.js b/static/skin/index.js index e0a063cac..507342dbb 100644 --- a/static/skin/index.js +++ b/static/skin/index.js @@ -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 ``; + 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 += ``; + }); + }); + } + + 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()} } })(); diff --git a/static/skin/isotope.pkgd.min.js b/static/skin/isotope.pkgd.min.js new file mode 100644 index 000000000..7ca671cbe --- /dev/null +++ b/static/skin/isotope.pkgd.min.js @@ -0,0 +1,12 @@ +/*! + * Isotope PACKAGED v3.0.6 + * + * Licensed GPLv3 for open source use + * or Isotope Commercial License for commercial use + * + * https://isotope.metafizzy.co + * Copyright 2010-2018 Metafizzy + */ + +!function(t,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("jquery")):t.jQueryBridget=e(t,t.jQuery)}(window,function(t,e){"use strict";function i(i,s,a){function u(t,e,o){var n,s="$()."+i+'("'+e+'")';return t.each(function(t,u){var h=a.data(u,i);if(!h)return void r(i+" not initialized. Cannot call methods, i.e. "+s);var d=h[e];if(!d||"_"==e.charAt(0))return void r(s+" is not a valid method");var l=d.apply(h,o);n=void 0===n?l:n}),void 0!==n?n:t}function h(t,e){t.each(function(t,o){var n=a.data(o,i);n?(n.option(e),n._init()):(n=new s(o,e),a.data(o,i,n))})}a=a||e||t.jQuery,a&&(s.prototype.option||(s.prototype.option=function(t){a.isPlainObject(t)&&(this.options=a.extend(!0,this.options,t))}),a.fn[i]=function(t){if("string"==typeof t){var e=n.call(arguments,1);return u(this,t,e)}return h(this,t),this},o(a))}function o(t){!t||t&&t.bridget||(t.bridget=i)}var n=Array.prototype.slice,s=t.console,r="undefined"==typeof s?function(){}:function(t){s.error(t)};return o(e||t.jQuery),i}),function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},o=i[t]=i[t]||[];return o.indexOf(e)==-1&&o.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{},o=i[t]=i[t]||{};return o[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var o=i.indexOf(e);return o!=-1&&i.splice(o,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){i=i.slice(0),e=e||[];for(var o=this._onceEvents&&this._onceEvents[t],n=0;n