Compare commits

...

6 Commits

Author SHA1 Message Date
Nikhil Tanwar
fc87def18b Add documentation for widget
Added documentation for current widget usage, currently supported arguments, using custom CSS/JS
2022-08-15 18:22:23 +05:30
Nikhil Tanwar
09dc6f90fd Add custom CSS and JS support
Added an event listener for message event.
The idea is the website which embeds the widget will send a message using postMessage().
The expected message is:
{
css: // custom CSS code
js: // custom JS code
}
2022-08-15 18:22:22 +05:30
Nikhil Tanwar
58c04b3f77 Basic widget handling
Adds handling for parameters:
disablefilter - disable search filters
disableclick - disable book click action
disabledownload - disable download button
disabledesc - disable description
2022-08-15 18:21:41 +05:30
Nikhil Tanwar
d27220f65d Give name to IIFE in index.js and expose updateBookCount
Gives a name to the IIFE wrapping code in index.js - kiwixServe
and exposes updateBookCount through it
2022-08-11 21:18:21 +05:30
Nikhil Tanwar
efe42c9bbe Add widget endpoint
Adds an endpoint /widget to provide kiwix serve widget.
2022-08-11 21:18:21 +05:30
Nikhil Tanwar
489dfc1123 Give a name to function for updating book count
Extracts function updateBookCount() from the unnamed function in resize event.
2022-08-11 21:10:47 +05:30
9 changed files with 309 additions and 22 deletions

View File

@@ -12,3 +12,4 @@ Welcome to libkiwix's documentation!
usage
api/ref_api
widget

82
docs/widget.rst Normal file
View File

@@ -0,0 +1,82 @@
Kiwix serve widget
====================
Introduction
------------
The kiwix-serve widget provides an easy to embed way to show the `kiwix-serve` homepage.
Usage
-----
To use the widget, simply add an iframe with its `src` attribute set to the `widget` endpoint.
Example HTML Page ::
<!DOCTYPE html>
<html lang="en">
<head>
<title>Widget Test</title>
</head>
<body>
<iframe src="http://192.168.18.8:8080/widget?disabledesc&disablefilter&disabledownload" width=1000 height=1000></iframe>
</body>
</html>
This creates an iframe with the kiwix-serve homepage contents.
Arguments are explained below.
Possible Arguments
-------------------
Currently, the following arguments are supported.
disabledesc (value = N/A)
Disables the description part of a tile.
disablefilter (value = N/A)
Disables the search filters: language, category, tag and search function.
disableclick (value = N/A)
Disables clicking the book to open it for reading.
disabledownload (value = N/A)
Disables the download button (if avaialable at all) on the tile.
Custom CSS and JS
-----------------
You can add your custom CSS rules and Javascript code to the widget.
To do that, use the following code as template::
<iframe id="receiver" src="http://192.168.18.8:8080/widget?disabledesc=&disablefilter=&disabledownload=" width="1000" height="1000">
<p>Your browser does not support iframes.</p>
</iframe>
<script>
window.onload = function() {
var receiver = document.getElementById('receiver').contentWindow;
function sendMessage() {
let msg = {
css: `
.book__header {
color:red;
}`,
js: `
function widgetTest() {
console.log("Testing widget");
}
widgetTest();
`
}
receiver.postMessage(msg, 'http://192.168.18.8:8080/widget');
}
sendMessage();
}
</script>
The CSS/JS fields are optional, you may send both or only one.

View File

@@ -577,6 +577,9 @@ std::unique_ptr<Response> InternalServer::handle_request(const RequestContext& r
if (isEndpointUrl(url, "catch"))
return handle_catch(request);
if (isEndpointUrl(url, "widget"))
return handle_widget(request);
std::string contentUrl = m_root + "/content" + url;
const std::string query = request.get_query();
if ( ! query.empty() )
@@ -866,6 +869,11 @@ std::unique_ptr<Response> InternalServer::handle_random(const RequestContext& re
}
}
std::unique_ptr<Response> InternalServer::handle_widget(const RequestContext& request)
{
return ContentResponse::build(*this, RESOURCE::templates::widget_html, get_default_data(), "text/html; charset=utf-8", true);
}
std::unique_ptr<Response> InternalServer::handle_captured_external(const RequestContext& request)
{
std::string source = "";

View File

@@ -143,6 +143,7 @@ class InternalServer {
std::unique_ptr<Response> handle_content(const RequestContext& request);
std::unique_ptr<Response> handle_raw(const RequestContext& request);
std::unique_ptr<Response> handle_locally_customized_resource(const RequestContext& request);
std::unique_ptr<Response> handle_widget(const RequestContext& request);
std::vector<std::string> search_catalog(const RequestContext& request,
kiwix::OPDSDumper& opdsDumper);

View File

@@ -9,6 +9,7 @@ skin/iso6391To3.js
skin/isotope.pkgd.min.js
skin/index.js
skin/autoComplete.min.js
skin/widget.js
skin/taskbar.css
skin/index.css
skin/fonts/Poppins.ttf
@@ -20,6 +21,7 @@ templates/search_result.xml
templates/error.html
templates/error.xml
templates/index.html
templates/widget.html
templates/suggestion.json
templates/head_taskbar.html
templates/taskbar_part.html

View File

@@ -1,4 +1,4 @@
(function() {
const kiwixServe = (function() {
const root = document.querySelector(`link[type='root']`).getAttribute('href');
const incrementalLoadingParams = {
start: 0,
@@ -17,6 +17,7 @@
let params = new URLSearchParams(window.location.search || filters || '');
let timer;
let languages = {};
let allowBookClick = true;
function queryUrlBuilder() {
let url = `${root}/catalog/search?`;
@@ -85,7 +86,7 @@
}
function generateBookHtml(book, sort = false) {
const link = book.querySelector('link[type="text/html"]').getAttribute('href');
let link = book.querySelector('link[type="text/html"]').getAttribute('href');
let iconUrl;
book.querySelectorAll('link[rel="http://opds-spec.org/image/thumbnail"]').forEach(link => {
if (link.getAttribute('type').split(';')[1] == 'width=48' && !iconUrl) {
@@ -120,6 +121,9 @@
}
const faviconAttr = iconUrl != undefined ? `style="background-image: url('${iconUrl}')"` : '';
const languageAttr = langCode != '' ? `title="${language}" aria-label="${language}"` : 'style="background-color: transparent"';
if (!allowBookClick) {
link = "javascript:void(0)";
}
divTag.innerHTML = `
<div class="book__wrapper">
<a class="book__link" href="${link}" data-hover="Preview">
@@ -247,14 +251,16 @@
toggleFooter();
}
const kiwixResultText = document.querySelector('.kiwixHomeBody__results')
if (results) {
let resultText = `${results} books`;
if (results === 1) {
resultText = `${results} book`;
if (kiwixResultText) {
if (results) {
let resultText = `${results} books`;
if (results === 1) {
resultText = `${results} book`;
}
kiwixResultText.innerHTML = resultText;
} else {
kiwixResultText.innerHTML = ``;
}
kiwixResultText.innerHTML = resultText;
} else {
kiwixResultText.innerHTML = ``;
}
loader.style.display = 'none';
return books;
@@ -265,16 +271,20 @@
await fetch(query).then(async (resp) => {
const data = new window.DOMParser().parseFromString(await resp.text(), 'application/xml');
let optionStr = '';
data.querySelectorAll('entry').forEach(entry => {
const title = getInnerHtml(entry, 'title');
const value = getInnerHtml(entry, valueEntryNode);
const hfTitle = humanFriendlyTitle(title);
if (valueEntryNode == 'language') {
languages[value] = hfTitle;
}
optionStr += (hfTitle != '') ? `<option value="${value}">${hfTitle}</option>` : '';
});
document.querySelector(nodeQuery).innerHTML += optionStr;
const entryList = data.querySelectorAll('entry');
const nodeQueryElem = document.querySelector(nodeQuery);
if (entryList && nodeQueryElem) {
entryList.forEach(entry => {
const title = getInnerHtml(entry, 'title');
const value = getInnerHtml(entry, valueEntryNode);
const hfTitle = humanFriendlyTitle(title);
if (valueEntryNode == 'language') {
languages[value] = hfTitle;
}
optionStr += (hfTitle != '') ? `<option value="${value}">${hfTitle}</option>` : '';
});
nodeQueryElem.innerHTML += optionStr;
}
});
}
@@ -388,6 +398,10 @@
}
});
}
function disableBookClick() {
allowBookClick = false;
}
function addTagElement(tagValue, resetFilter) {
const tagElement = document.getElementsByClassName('tagFilterLabel')[0];
@@ -429,13 +443,15 @@
}
}
window.addEventListener('resize', (event) => {
function updateBookCount(event) {
if (timer) {clearTimeout(timer)}
timer = setTimeout(() => {
incrementalLoadingParams.count = incrementalLoadingParams.count && viewPortToCount();
loadSubset();
}, 100, event);
});
}
window.addEventListener('resize', (event) => updateBookCount(event));
window.addEventListener('scroll', loadSubset);
@@ -479,6 +495,7 @@
}
}
updateVisibleParams();
updateBookCount();
document.getElementById('kiwixSearchForm').onsubmit = (event) => {event.preventDefault()};
if (!window.location.search) {
const browserLang = navigator.language.split('-')[0];
@@ -491,5 +508,10 @@
}
setCookie(filterCookieName, params.toString());
}
return {
updateBookCount,
disableBookClick
};
})();

107
static/skin/widget.js Normal file
View File

@@ -0,0 +1,107 @@
function disableSearchFilters(widgetStyles) {
const hideNavRule = `
.kiwixNav {
display: none;
}`;
const hideResultsLabelRule = `
.kiwixHomeBody__results {
display: none;
}`;
const hideTagFilterRule = `
.book__tags {
pointer-events: none;
}`;
insertNewCssRules(widgetStyles, [hideNavRule, hideResultsLabelRule, hideTagFilterRule]);
}
function disableBookClick() {
kiwixServe.disableBookClick();
}
function disableDownload(widgetStyles) {
const hideBookDownloadRule = `
.book__download {
display: none;
}`;
insertNewCssRules(widgetStyles, [hideBookDownloadRule]);
}
function disableDescription(widgetStyles) {
const decreaseHeightRule = `
.book__wrapper {
height:128px;
grid-template-rows: 70px 0 1fr 1fr;
}`;
const hideDescRule = `
.book__description {
display: none;
}`;
insertNewCssRules(widgetStyles, [decreaseHeightRule, hideDescRule]);
}
function hideFooter(widgetStyles) {
const hideFooterRule = `
.kiwixfooter {
display: none !important;
}`;
insertNewCssRules(widgetStyles, [hideFooterRule]);
}
function insertNewCssRules(stylesheet, ruleList) {
if (stylesheet) {
for (rule of ruleList) {
stylesheet.insertRule(rule, 0);
}
}
}
function addCustomCss(cssCode) {
let customCSS = document.createElement('style');
customCSS.innerHTML = cssCode;
document.head.appendChild(customCSS);
}
function addCustomJs(jsCode) {
new Function(`"use strict";${jsCode}`)();
}
function handleMessages(event) {
if ('css' in event.data) {
addCustomCss(event.data.css);
}
if ('js' in event.data) {
addCustomJs(event.data.js);
}
}
function handleWidget() {
const params = new URLSearchParams(window.location.search || filters || '');
const widgetStyleElem = document.createElement('style');
document.head.appendChild(widgetStyleElem);
const widgetStyles = widgetStyleElem.sheet;
const disableFilters = params.has('disablefilter');
const disableClick = params.has('disableclick');
const disableDwld = params.has('disabledownload');
const disableDesc = params.has('disabledesc');
const blankBase = document.createElement('base');
blankBase.target = '_blank';
document.head.appendChild(blankBase); // open all links in new tab
if (disableFilters)
disableSearchFilters(widgetStyles);
if (disableClick)
disableBookClick();
if (disableDwld)
disableDownload(widgetStyles);
if (disableDesc)
disableDescription(widgetStyles);
hideFooter(widgetStyles);
kiwixServe.updateBookCount();
}
window.addEventListener('message', handleMessages);
handleWidget();

View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Welcome to Kiwix Server</title>
<link
type="text/css"
href="{{root}}/skin/index.css?KIWIXCACHEID"
rel="Stylesheet"
/>
<style>
@font-face {
font-family: "poppins";
src: url("{{root}}/skin/fonts/Poppins.ttf?KIWIXCACHEID") format("truetype");
}
@font-face {
font-family: "roboto";
src: url("{{root}}/skin/fonts/Roboto.ttf?KIWIXCACHEID") format("truetype");
}
</style>
<script src="{{root}}/skin/isotope.pkgd.min.js?KIWIXCACHEID" defer></script>
<script src="{{root}}/skin/iso6391To3.js?KIWIXCACHEID"></script>
<script type="text/javascript" src="{{root}}/skin/index.js?KIWIXCACHEID" defer></script>
<script type="text/javascript" src="{{root}}/skin/widget.js?KIWIXCACHEID" defer></script>
</head>
<body>
<div class='kiwixNav'>
<div class="kiwixNav__filters">
<div class="kiwixNav__select">
<select name="lang" id="languageFilter" class='kiwixNav__kiwixFilter filter'>
<option value="" selected>All languages</option>
</select>
</div>
<div class="kiwixNav__select">
<select name="category" id="categoryFilter" class='kiwixNav__kiwixFilter filter'>
<option value="" selected>All categories</option>
</select>
</div>
</div>
<form id='kiwixSearchForm' class='kiwixNav__SearchForm'>
<input type="text" name="q" placeholder="Search" id="searchFilter" class='kiwixSearch filter'>
<span class="kiwixButton tagFilterLabel"></span>
<input type="submit" class="kiwixButton kiwixButtonHover" value="Search"/>
</form>
</div>
<div class="kiwixHomeBody">
<div class="book__list">
<h3 class="kiwixHomeBody__results"></h3>
</div>
<div id="fadeOut" class="fadeOut"></div>
</div>
<div class="loader" style="position: absolute; top: 50%"><div class="loader-spinner"></div></div>
<div id="kiwixfooter" class="kiwixfooter">Powered by&nbsp;<a href="https://kiwix.org">Kiwix</a></div>
</body>
<script>
function closeModal() {
for(modal of document.getElementsByClassName('modal-wrapper')) {
modal.remove();
}
}
</script>
</html>

View File

@@ -184,7 +184,7 @@ R"EXPECTEDRESULT( href="/ROOT/skin/index.css?cacheid=56e818cd"
src: url("/ROOT/skin/fonts/Roboto.ttf?cacheid=84d10248") format("truetype");
<script src="/ROOT/skin/isotope.pkgd.min.js?cacheid=2e48d392" defer></script>
<script src="/ROOT/skin/iso6391To3.js?cacheid=ecde2bb3"></script>
<script type="text/javascript" src="/ROOT/skin/index.js?cacheid=76440e7a" defer></script>
<script type="text/javascript" src="/ROOT/skin/index.js?cacheid=2fcc4ac4" defer></script>
)EXPECTEDRESULT"
},
{