libreblog
Version:
An in-browser Static Site Generator focused on content-rich blogs and news websites
1,191 lines (1,036 loc) • 168 kB
JavaScript
/**
* Libreblog.org
* Copyright (c) 2025 Thiago Gomes Quinalia
* This code is licensed under the MIT License.
* License text available at https://opensource.org/license/MIT
*/
'use strict';
(function(){
/* Constants and default values */
const debug_mode = false;
const thumbnail_column = "Thumbnail";
const supported_formats = ["JPG", "JPEG", "PNG", "APNG", "GIF", "SVG", "WEBP", "AVIF", "ICO"];
const supported_version_formats = ["JPG", "JPEG", "PNG", "WEBP"];
const jszip_version = "3.10.1";
const ace_builds_version = "1.41.0";
const default_footer_social_snippet = `<a href="PASTE YOUR NEWSLETTER URL HERE" id="subscribe-button" target="_blank"> Subscribe </a>
<!-- Simply enter the URLs of your social media pages in the segments marked in capital letters. You can delete any unused platform sections, which are indicated by the comments: "[Platform]:start" and "[Platform]:end". For SVGs of other social media platforms, visit svgrepo.com . -->
<div id="footer-social-icons">
<div id="first-icons-section">
<!-- Facebook:start -->
<a href="PASTE YOUR FACEBOOK URL HERE" target="_blank" title="Follow us on Facebook">
<svg version="1.1" id="layer_facebook" class="social-icon-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 54 54" xml:space="preserve">
<g>
<rect class="social-icon-rect" x="0" y="0" width="54" height="54"/>
<path d="M40,4.8c2.2,0,4.1,0.8,5.7,2.4c1.6,1.6,2.4,3.5,2.4,5.7v26.8c0,2.2-0.8,4.1-2.4,5.7c-1.6,1.6-3.5,2.4-5.7,2.4h-5.2V31.1
h5.6l0.8-6.5h-6.4v-4.1c0-1,0.2-1.8,0.7-2.3c0.4-0.5,1.3-0.8,2.6-0.8l3.4,0v-5.8c-1.2-0.2-2.8-0.3-5-0.3c-2.5,0-4.6,0.7-6.1,2.2
c-1.5,1.5-2.3,3.6-2.3,6.3v4.8h-5.6v6.5h5.6v16.6H13.3c-2.2,0-4.1-0.8-5.7-2.4c-1.6-1.6-2.4-3.5-2.4-5.7V12.9
c0-2.2,0.8-4.1,2.4-5.7c1.6-1.6,3.5-2.4,5.7-2.4H40z"/>
</g>
</svg>
</a>
<!-- Facebook:end -->
<!-- X/Twitter:start -->
<a href="PASTE YOUR X/TWITTER URL HERE" target="_blank" title="Follow us on X/Twitter">
<svg version="1.1" id="layer_x" class="social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect class="social-icon-rect" x="0" y="0" width="512" height="512"/>
<path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/>
</svg>
</a>
<!-- X/Twitter:end -->
<!-- Instagram:start -->
<a href="PASTE YOUR INSTAGRAM URL HERE" target="_blank" title="Follow us on Instagram">
<svg version="1.1" id="layer_instagram" class="social-icon-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 20 20" xml:space="preserve">
<g>
<rect class="social-icon-rect" x="0" y="0" width="20" height="20"/>
<path d="M12.7 10c0-1.5-1.2-2.7-2.7-2.7S7.3 8.5 7.3 10s1.2 2.7 2.7 2.7c1.5 0 2.7-1.2 2.7-2.7zm1.4 0c0 2.3-1.8 4.1-4.1 4.1S5.9 12.3 5.9 10 7.7 5.9 10 5.9s4.1 1.8 4.1 4.1zm1.1-4.3c0 .6-.4 1-1 1s-1-.4-1-1 .4-1 1-1 1 .5 1 1zM10 3.4c-1.2 0-3.7-.1-4.7.3-.7.3-1.3.9-1.5 1.6-.4 1-.3 3.5-.3 4.7s-.1 3.7.3 4.7c.2.7.8 1.3 1.5 1.5 1 .4 3.6.3 4.7.3s3.7.1 4.7-.3c.7-.3 1.2-.8 1.5-1.5.4-1.1.3-3.6.3-4.7s.1-3.7-.3-4.7c-.2-.7-.8-1.3-1.5-1.5-1-.5-3.5-.4-4.7-.4zm8 6.6v3.3c0 1.2-.4 2.4-1.3 3.4-.9.9-2.1 1.3-3.4 1.3H6.7c-1.2 0-2.4-.4-3.4-1.3-.8-.9-1.3-2.1-1.3-3.4V10 6.7c0-1.3.5-2.5 1.3-3.4C4.3 2.5 5.5 2 6.7 2h6.6c1.2 0 2.4.4 3.4 1.3.8.9 1.3 2.1 1.3 3.4V10z"/>
</g>
</svg>
</a>
<!-- Instagram:end -->
<!-- YouTube:start -->
<a href="PASTE YOUR YOUTUBE URL HERE" target="_blank" title="Subscribe to our channel on YouTube">
<svg version="1.1" id="layer_youtube" class="social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">
<rect class="social-icon-rect" x="0" y="0" width="600" height="600"/>
<path transform="translate(12, 44)" d="M549.7 124.1c-6.3-23.7-24.8-42.3-48.3-48.6C458.8 64 288 64 288 64S117.2 64 74.6 75.5c-23.5 6.3-42 24.9-48.3 48.6-11.4 42.9-11.4 132.3-11.4 132.3s0 89.4 11.4 132.3c6.3 23.7 24.8 41.5 48.3 47.8C117.2 448 288 448 288 448s170.8 0 213.4-11.5c23.5-6.3 42-24.2 48.3-47.8 11.4-42.9 11.4-132.3 11.4-132.3s0-89.4-11.4-132.3zm-317.5 213.5V175.2l142.7 81.2-142.7 81.2z"/>
</svg>
</a>
<!-- YouTube:end -->
</div>
<!-- "See more" button -->
<details id="more-icons-section">
<summary><span id="more-icons-btn">+</span></summary>
<div id="more-icons-div">
<!-- Keep below the icons that will be hidden and only displayed if the user clicks on the "See more" button -->
<!-- LinkedIn:start -->
<a href="PASTE YOUR LINKEDIN URL HERE" target="_blank" title="Connect with us on LinkedIn">
<svg version="1.1" id="layer_linkedin" class="social-icon-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 54 54" xml:space="preserve">
<g>
<rect class="social-icon-rect" x="0" y="0" width="54" height="54"/>
<path d="M48.1,12.9v26.8c0,2.2-0.8,4.1-2.4,5.7c-1.6,1.6-3.5,2.4-5.7,2.4H13.3c-2.2,0-4.1-0.8-5.7-2.4c-1.6-1.6-2.4-3.5-2.4-5.7
V12.9c0-2.2,0.8-4.1,2.4-5.7c1.6-1.6,3.5-2.4,5.7-2.4H40c2.2,0,4.1,0.8,5.7,2.4C47.3,8.8,48.1,10.7,48.1,12.9z M18.7,15.4
c0-1-0.4-1.8-1-2.4C17,12.3,16.2,12,15.1,12c-1.1,0-2,0.3-2.6,0.9c-0.7,0.6-1,1.4-1,2.4c0,0.9,0.3,1.7,1,2.4c0.7,0.6,1.5,1,2.6,1h0
c1.1,0,2-0.3,2.7-1C18.4,17.1,18.7,16.3,18.7,15.4z M11.8,40.7h6.4V21.3h-6.4V40.7z M35,40.7h6.4V29.6c0-2.9-0.7-5-2-6.5
c-1.4-1.5-3.2-2.2-5.4-2.2c-2.5,0-4.5,1.1-5.8,3.3h0.1v-2.8h-6.4c0.1,1.2,0.1,7.7,0,19.4h6.4V29.9c0-0.7,0.1-1.2,0.2-1.6
c0.3-0.7,0.7-1.2,1.3-1.7c0.6-0.5,1.2-0.7,2.1-0.7c2.2,0,3.2,1.5,3.2,4.4V40.7z"/>
</g>
</svg>
</a>
<!-- LinkedIn:end -->
<!-- Tiktok:start -->
<a href="PASTE YOUR TIKTOK URL HERE" target="_blank" title="Follow us on Tiktok">
<svg version="1.1" id="layer_tiktok" class="social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect class="social-icon-rect" x="0" y="0" width="24" height="24"/>
<path d="M21,2H3A1,1,0,0,0,2,3V21a1,1,0,0,0,1,1H21a1,1,0,0,0,1-1V3A1,1,0,0,0,21,2Zm-3.281,8.725h0c-.109.011-.219.016-.328.017A3.571,3.571,0,0,1,14.4,9.129v5.493a4.061,4.061,0,1,1-4.06-4.06c.085,0,.167.008.251.013v2a2.067,2.067,0,1,0-.251,4.119A2.123,2.123,0,0,0,12.5,14.649l.02-9.331h1.914A3.564,3.564,0,0,0,17.719,8.5Z"/>
</svg>
</a>
<!-- Tiktok:end -->
<!-- WhatsApp:start -->
<a href="PASTE YOUR WHATSAPP URL HERE" target="_blank" title="Join us on WhatsApp">
<svg version="1.1" id="layer_whatsapp" class="social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54">
<rect class="social-icon-rect" x="0" y="0" width="54" height="54"/>
<path d="M18.9,6.3c2.6-1.1,5.3-1.7,8.2-1.7c2.8,0,5.6,0.6,8.2,1.7c2.6,1.1,4.8,2.6,6.7,4.5c1.9,1.9,3.4,4.1,4.5,6.7
c1.1,2.6,1.7,5.3,1.7,8.2c0,2.8-0.6,5.6-1.7,8.2s-2.6,4.8-4.5,6.7c-1.9,1.9-4.1,3.4-6.7,4.5s-5.3,1.7-8.2,1.7
c-3.6,0-7-0.9-10.2-2.6L5.2,47.9L9,36.6c-2-3.3-3-6.9-3-10.9c0-2.8,0.6-5.6,1.7-8.2c1.1-2.6,2.6-4.8,4.5-6.7
C14,8.9,16.3,7.5,18.9,6.3z M27,43.2c2.4,0,4.6-0.5,6.8-1.4c2.2-0.9,4-2.2,5.6-3.7s2.8-3.4,3.7-5.6s1.4-4.4,1.4-6.8
s-0.5-4.6-1.4-6.8c-0.9-2.2-2.2-4-3.7-5.6s-3.4-2.8-5.6-3.7c-2.2-0.9-4.4-1.4-6.8-1.4c-2.4,0-4.6,0.5-6.8,1.4
c-2.2,0.9-4,2.2-5.6,3.7s-2.8,3.4-3.7,5.6c-0.9,2.2-1.4,4.4-1.4,6.8c0,3.8,1.1,7.2,3.3,10.3l-2.2,6.5l6.8-2.1
C20.3,42.3,23.6,43.2,27,43.2z M31.2,29.8c0.7-0.9,1.2-1.4,1.5-1.4c0.2,0,1.1,0.4,2.7,1.2c1.6,0.8,2.4,1.3,2.5,1.5
c0,0.1,0.1,0.2,0.1,0.4c0,0.6-0.2,1.3-0.5,2.1c-0.3,0.7-1,1.3-2,1.8c-1,0.5-2,0.7-2.8,0.7c-1.1,0-2.8-0.6-5.3-1.7
c-1.8-0.8-3.4-1.9-4.7-3.3c-1.3-1.4-2.7-3.1-4.1-5.2c-1.3-2-2-3.8-2-5.4v-0.2c0.1-1.7,0.7-3.2,2.1-4.4c0.4-0.4,0.9-0.6,1.5-0.6
c0.1,0,0.3,0,0.5,0c0.2,0,0.4,0,0.5,0c0.4,0,0.6,0.1,0.7,0.2c0.1,0.1,0.3,0.4,0.4,0.8c0.1,0.4,0.5,1.2,0.9,2.5
c0.5,1.3,0.7,2,0.7,2.1c0,0.4-0.3,0.9-1,1.6c-0.6,0.7-1,1.1-1,1.3c0,0.1,0,0.3,0.1,0.4c0.6,1.4,1.6,2.6,2.8,3.8
c1,1,2.4,1.9,4.2,2.8c0.2,0.1,0.4,0.2,0.6,0.2C30,31.1,30.5,30.7,31.2,29.8z"/>
</svg>
</a>
<!-- WhatsApp:end -->
<!-- Threads:start -->
<a href="PASTE YOUR THREADS URL HERE" target="_blank" title="Follow us on Threads">
<svg version="1.1" id="layer_threads" class="social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 960">
<rect class="social-icon-rect" x="0" y="0" width="960" height="960"/>
<path d="M404.63 392.13c-11.92-7.93-51.53-35.49-51.53-35.49 33.4-47.88 77.46-66.52 138.36-66.52 43.07 0 79.64 14.52 105.75 42 26.12 27.49 41.02 66.8 44.41 117.07 14.48 6.07 27.85 13.22 39.99 21.4 48.96 33 75.92 82.34 75.92 138.91 0 120.23-98.34 224.67-276.35 224.67-152.84 0-311.63-89.11-311.63-354.45 0-263.83 153.81-353.92 311.2-353.92 72.68 0 243.16 10.76 307.27 222.94l-60.12 15.63C678.33 213.2 574.4 189.14 479.11 189.14c-157.52 0-246.62 96.13-246.62 300.65 0 183.38 99.59 280.8 248.71 280.8 122.68 0 214.15-63.9 214.15-157.44 0-63.66-53.37-94.14-56.1-94.14-10.42 54.62-38.36 146.5-161.01 146.5-71.46 0-133.07-49.47-133.07-114.29 0-92.56 87.61-126.06 156.8-126.06 25.91 0 57.18 1.75 73.46 5.07 0-28.21-23.81-76.49-83.96-76.49-55.15-.01-69.14 17.92-86.84 38.39zm105.8 96.25c-90.13 0-101.79 38.51-101.79 62.7 0 38.86 46.07 51.74 70.65 51.74 45.06 0 91.35-12.52 98.63-107.31-22.85-5.14-39.88-7.13-67.49-7.13z"/>
</svg>
</a>
<!-- Threads:end -->
<!-- Telegram:start -->
<a href="PASTE YOUR TELEGRAM URL HERE" target="_blank" title="Join us on Telegram">
<svg version="1.1" id="layer_telegram" class="social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58">
<rect class="social-icon-rect" x="0" y="0" width="58" height="58"/>
<path transform="translate(2, 2)"
d="M49.7,16.5c1.3,3.1,2,6.3,2,9.7s-0.7,6.6-2,9.7c-1.3,3.1-3.1,5.7-5.3,8c-2.2,2.2-4.9,4-8,5.3c-3.1,1.3-6.3,2-9.7,2
c-3.4,0-6.6-0.7-9.7-2s-5.7-3.1-8-5.3c-2.2-2.2-4-4.9-5.3-8c-1.3-3.1-2-6.3-2-9.7s0.7-6.6,2-9.7c1.3-3.1,3.1-5.7,5.3-8
c2.2-2.2,4.9-4,8-5.3s6.3-2,9.7-2c3.4,0,6.6,0.7,9.7,2c3.1,1.3,5.7,3.1,8,5.3C46.6,10.8,48.3,13.5,49.7,16.5z M34.8,37.7l4.1-19.3
c0.2-0.8,0.1-1.4-0.3-1.8c-0.4-0.4-0.8-0.4-1.4-0.2l-24.1,9.3c-0.5,0.2-0.9,0.4-1.1,0.7c-0.2,0.3-0.2,0.5-0.1,0.7
c0.1,0.2,0.4,0.4,0.9,0.5l6.2,1.9l14.3-9c0.4-0.3,0.7-0.3,0.9-0.2c0.1,0.1,0.1,0.2-0.1,0.4L22.5,31.3L22,37.7
c0.4,0,0.8-0.2,1.3-0.6l3-2.9l6.2,4.6C33.8,39.5,34.5,39.1,34.8,37.7z"/>
</svg>
</a>
<!-- Telegram:end -->
<!-- Bluesky:start -->
<a href="PASTE YOUR BLUESKY URL HERE" target="_blank" title="Follow us on Bluesky">
<svg version="1.1" id="layer_bluesky" class="social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect class="social-icon-rect" x="0" y="0" width="512" height="512"/>
<path transform="translate(32, 0)" d="M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zM224 247.4c14.5-30 54-85.8 90.7-113.3c26.5-19.9 69.3-35.2 69.3 13.7c0 9.8-5.6 82.1-8.9 93.8c-11.4 40.8-53 51.2-90 44.9c64.7 11 81.2 47.5 45.6 84c-67.5 69.3-97-17.4-104.6-39.6c0 0 0 0 0 0l-.3-.9c-.9-2.6-1.4-4.1-1.8-4.1s-.9 1.5-1.8 4.1c-.1 .3-.2 .6-.3 .9c0 0 0 0 0 0c-7.6 22.2-37.1 108.8-104.6 39.6c-35.5-36.5-19.1-73 45.6-84c-37 6.3-78.6-4.1-90-44.9c-3.3-11.7-8.9-84-8.9-93.8c0-48.9 42.9-33.5 69.3-13.7c36.7 27.5 76.2 83.4 90.7 113.3z"/>
</svg>
</a>
<!-- Bluesky:end -->
<!-- Mastodon:start -->
<a href="PASTE YOUR MASTODON URL HERE" target="_blank" title="Follow us on Mastodon">
<svg version="1.1" id="layer_mastodon" class="social-icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect class="social-icon-rect" x="0" y="0" width="24" height="24"/>
<path d="M21.327 8.566c0-4.339-2.843-5.61-2.843-5.61-1.433-.658-3.894-.935-6.451-.956h-.063c-2.557.021-5.016.298-6.45.956 0 0-2.843 1.272-2.843 5.61 0 .993-.019 2.181.012 3.441.103 4.243.778 8.425 4.701 9.463 1.809.479 3.362.579 4.612.51 2.268-.126 3.541-.809 3.541-.809l-.075-1.646s-1.621.511-3.441.449c-1.804-.062-3.707-.194-3.999-2.409a4.523 4.523 0 0 1-.04-.621s1.77.433 4.014.536c1.372.063 2.658-.08 3.965-.236 2.506-.299 4.688-1.843 4.962-3.254.434-2.223.398-5.424.398-5.424zm-3.353 5.59h-2.081V9.057c0-1.075-.452-1.62-1.357-1.62-1 0-1.501.647-1.501 1.927v2.791h-2.069V9.364c0-1.28-.501-1.927-1.502-1.927-.905 0-1.357.546-1.357 1.62v5.099H6.026V8.903c0-1.074.273-1.927.823-2.558.566-.631 1.307-.955 2.228-.955 1.065 0 1.872.409 2.405 1.228l.518.869.519-.869c.533-.819 1.34-1.228 2.405-1.228.92 0 1.662.324 2.228.955.549.631.822 1.484.822 2.558v5.253z"/>
</svg>
</a>
<!-- Mastodon:end -->
<!-- Github:start -->
<a href="PASTE YOUR GITHUB URL HERE" target="_blank" title="Star our repository on Github">
<svg id="layer_github" class="social-icon-svg" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect class="social-icon-rect" x="0" y="0" width="24" height="24"/>
<path d="M12,2A10,10,0,0,0,8.84,21.5c.5.08.66-.23.66-.5V19.31C6.73,19.91,6.14,18,6.14,18A2.69,2.69,0,0,0,5,16.5c-.91-.62.07-.6.07-.6a2.1,2.1,0,0,1,1.53,1,2.15,2.15,0,0,0,2.91.83,2.16,2.16,0,0,1,.63-1.34C8,16.17,5.62,15.31,5.62,11.5a3.87,3.87,0,0,1,1-2.71,3.58,3.58,0,0,1,.1-2.64s.84-.27,2.75,1a9.63,9.63,0,0,1,5,0c1.91-1.29,2.75-1,2.75-1a3.58,3.58,0,0,1,.1,2.64,3.87,3.87,0,0,1,1,2.71c0,3.82-2.34,4.66-4.57,4.91a2.39,2.39,0,0,1,.69,1.85V21c0,.27.16.59.67.5A10,10,0,0,0,12,2Z"/>
</svg>
</a>
<!-- Github:end -->
</div>
</details>
</div>`;
const default_main_js =
`/** Libreblog's core features **/
const searchInput = document.getElementById("search-input");
const searchBoxWrapper = document.getElementById("search-box-wrapper");
const shareButton = document.getElementById("share-button");
const itemDates = document.getElementsByClassName("item-date");
const formatDate = function(dateStr, options) {
const dateObj = new Date(dateStr);
const lang = navigator && navigator.language ? navigator.language : "en";
let formattedDate = new Intl.DateTimeFormat(lang, options).format(dateObj);
if (lang.startsWith("pt")) {
formattedDate = formattedDate.replaceAll("de ", "");
}
return formattedDate;
}
if (itemDates && itemDates.length > 0) {
for (let i = 0; i < itemDates.length; i++) {
const itemDate = itemDates[i];
if (itemDate.dateTime.split("T")[0] === new Date().toISOString().split("T")[0]) {
itemDate.innerText = formatDate(itemDate.dateTime, { hour: 'numeric', minute: 'numeric' }) + " " + itemDate.attributes["timezone-abbreviation"].value;
} else {
itemDate.innerText = formatDate(itemDate.dateTime, { year: 'numeric', month: 'short', day: 'numeric' });
}
}
}
if (shareButton) {
const url = shareButton.attributes["data-url"].value;
const title = shareButton.attributes["data-title"].value;
const data = {url: url, title: title};
if (navigator.canShare && navigator.canShare(data)) {
shareButton.addEventListener("click", async (event) => {
try {
await navigator.share(data);
} catch (e) {
console.log(e);
}
});
} else {
shareButton.style.display = "none";
}
}
if (searchInput && searchBoxWrapper) searchInput.addEventListener("input", (event) => {
let value = event.target.value;
if (value) {
value = value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
let innerHTML = "<section id='search-results-box'>";
const filtered = libreblogSearch.filter((item) => {
//Simple search function (exact match)
if (item.searchable && item.searchable.includes(value) && item['in_sitemap']) return true;
return false;
});
if (filtered.length === 0) {
innerHTML = "";
} else {
filtered.forEach((item) => {
const p = item.published ? item.published : item.created;
const d = new Date(p.replace(" ", "T"));
const lang = navigator && navigator.language ? navigator.language : "en";
const published = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric'}).format(d);
innerHTML += "<article class='search-card'>" +
"<a class='search-link' href='" + item.uri + "'>" +
"<div class='search-title'>" + item.title + "</div>" +
"<div class='search-published'>" + published + "</div>" +
"<div class='search-subtitle'>" + item.subtitle + "</div>" +
"</a>" +
"</article>";
});
innerHTML += "</section>";
}
searchBoxWrapper.innerHTML = innerHTML;
} else {
searchBoxWrapper.innerHTML = "";
}
});
/** Insert custom code below **/
`;
const default_htaccess = `DirectoryIndex index.html
Options -Indexes
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript
</IfModule>
<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 month"
ExpiresByType text/html "access plus 1 hour"
</IfModule>
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-XSS-Protection "1; mode=block"
Header set X-Frame-Options "DENY"
Header set X-Permitted-Cross-Domain-Policies "none"
Header set Permissions-Policy "geolocation=(), camera=(), microphone=()"
</IfModule>
`;
/* Funtions */
const generateJsonDb = function() {
globalThis.lb.useSqlite(async (db) => {
const jsonDb = {
sections: await globalThis.lb.getAllResultRows("sections", db, null),
series: await globalThis.lb.getAllResultRows("series", db, null),
articles: await globalThis.lb.getAllResultRows("articles", db, null),
relations: await globalThis.lb.getAllResultRows("relations", db, null),
authors: await globalThis.lb.getAllResultRows("authors", db, null),
navbar_items: await globalThis.lb.getAllResultRows("navbar_items", db, null),
media: await globalThis.lb.getAllResultRows("media", db, null),
sources: await globalThis.lb.getAllResultRows("sources", db, null),
templates: await globalThis.lb.getAllResultRows("templates", db, null),
settings: await globalThis.lb.getAllResultRows("settings", db, null)
};
const jsonDbStr = JSON.stringify(jsonDb, null, 2);
const blob = new Blob([jsonDbStr], {type: "text/plain;charset=utf-8"});
saveFile(blob, "mydb.json");
});
}
const insertThemeFromObject = async function(db, theme, themeUri) {
let templates = structuredClone(theme.templates);
templates.push({
template_uri: "image",
template_set: "",
template_type: "",
content_type: "cdata",
contents: theme["image"] ? theme["image"] : ""
});
templates.push({
template_uri: "config",
template_set: "",
template_type: "",
content_type: "json",
contents: JSON.stringify(theme["config"])
});
for (let i = 0; i < templates.length; i++) {
const template = templates[i];
await db.exec({
sql: "INSERT INTO templates ( " +
"theme_uri, " +
"template_uri, " +
"template_set, " +
"template_type, " +
"content_type, " +
"contents, " +
"created, " +
"updated " +
") VALUES ('" + themeUri + "', ?, ?, ?, ?, ?, " +
"STRFTIME('%Y-%m-%d %H:%M', DATETIME('now','localtime')), " +
"STRFTIME('%Y-%m-%d %H:%M', DATETIME('now','localtime')))",
bind: [
template['template_uri'],
template['template_set'],
template['template_type'],
template['content_type'],
template['contents']
]
});
}
};
const createThemeObjectFromDb = async function(db, themeUri) {
const theme = {theme_uri: themeUri, config: "{}", templates: [], image: ""};
const resultRows = await db.exec({
sql: "SELECT * FROM templates WHERE theme_uri = '" + themeUri + "'",
rowMode: 'object'
});
for (let i = 0; i < resultRows.length; i++) {
if (resultRows[i]["template_uri"] === "image") {
theme["image"] = resultRows[i]["contents"];
} else if (resultRows[i]["template_uri"] === "config") {
try {
theme["config"] = JSON.parse(resultRows[i]["contents"]);
} catch (e) {
globalThis.console.error("It was not possible to load this configuration. Error: ", e);
}
} else {
theme["templates"].push({
template_uri: resultRows[i]["template_uri"],
template_set: resultRows[i]["template_set"],
template_type: resultRows[i]["template_type"],
content_type: resultRows[i]["content_type"],
contents: resultRows[i]["contents"]
});
}
}
return theme;
}
const addDisabledOption = function(selectFilter) {
let option = globalThis.document.createElement("option");
option.innerText = "Filter by...";
option.value = "none";
selectFilter.appendChild(option);
}
const generateHeaderStr = function(id, args) {
let header = "<tr style='display:run-in' class='checkbox-td'><th><input id='th-checkbox' type='checkbox'></th>";
for (let i = 0; i < args.length; i++) {
const column = args[i][1];
header += "<th class='" + getTdClass(id, column)[0] + "'><div>" + column + (column !== thumbnail_column ? " <span><img class='data-table-icon' src='images/chevron-down.svg' /></span>" : "") + "</div></th>";
}
header += "</tr>";
return header;
}
const generateHeader = function(id, args) {
let selectFilter = globalThis.document.getElementById('select-filter');
while (selectFilter.firstChild) {
selectFilter.removeChild(selectFilter.lastChild);
}
addDisabledOption(selectFilter);
const header = generateHeaderStr(id, args);
let columns = [];
for (let i = 0; i < args.length; i++) {
columns.push(args[i][0]);
if (args[i][1] !== thumbnail_column) {
let option = globalThis.document.createElement("option");
option.innerText = args[i][1];
option.value = args[i][1];
selectFilter.appendChild(option);
}
}
return [header, columns];
}
const titleIfBig = function(text, size) {
if (text && text.length > size) return text;
return "";
}
const mobileScreen = function() {
return window.innerWidth <= 1024;
}
const smallScreen = function() {
return window.innerWidth < 1360;
}
const bigScreen = function() {
return window.innerWidth > 1530;
}
const veryBigScreen = function() {
return window.innerWidth > 1900;
}
const bigTd = function(tableId, columnName) {
let big = false;
switch (tableId) {
case "sources-table":
if (["Title"].includes(columnName)) {
big = true;
} else if (["Author"].includes(columnName) && bigScreen()) {
big = true;
}
break;
case "authors-table":
if (["Email", "Name"].includes(columnName) && bigScreen()) big = true;
break;
case "media-table":
if (["Alt Text"].includes(columnName) && bigScreen()) big = true;
break;
case "navbar-table":
if (["Label", "ID or URL"].includes(columnName) && bigScreen()) big = true;
break;
case "series-table":
if (["Title"].includes(columnName) && bigScreen()) big = true;
break;
case "sections-table":
if (["Title"].includes(columnName) && bigScreen()) big = true;
break;
case "articles-table":
if (["Title"].includes(columnName) && veryBigScreen()) big = true;
break;
default:
}
return big;
}
const smallTd = function(tableId, columnName) {
let small = false;
switch (tableId) {
case "modal-references-table":
if (["Year", "Type"].includes(columnName)) small = true;
break;
case "articles-table":
if (["Type", "Status"].includes(columnName)) {
small = true;
} else if (["Section", "Series", "Author(s)"].includes(columnName) && !bigScreen()) {
small = true;
}
break;
case "series-table":
if (["Status"].includes(columnName)) small = true;
break;
case "relations-table":
if (["Type"].includes(columnName)) small = true;
break;
case "sources-table":
if (["Type", "Year"].includes(columnName)) small = true;
break;
case "media-table":
if (["File Ext."].includes(columnName)) small = true;
break;
default:
}
return small;
}
const modestTd = function(tableId, columnName) {
let modest = false;
switch (tableId) {
case "articles-table":
if (["Title"].includes(columnName) && !bigScreen()) {
modest = true;
} else if (["Section", "Series", "Author(s)"].includes(columnName) && !veryBigScreen()) {
modest = true;
}
break;
case "series-table":
if (["Section"].includes(columnName) && !veryBigScreen()) modest = true;
break;
case "relations-table":
if (["Article 1", "Article 2"].includes(columnName) && !veryBigScreen()) modest = true;
break;
case "media-table":
if (["Size (bytes)"].includes(columnName)) modest = true;
break;
default:
if (["ID"].includes(columnName) && !veryBigScreen()) modest = true;
}
return modest;
}
const escapeHTML = function(text) {
if (typeof text !== "string") return text;
return text.replace(
/[&<>'"]/g,
tag =>
({
'&': '&',
'<': '<',
'>': '>',
"'": ''',
'"': '"'
}[tag] || tag)
);
}
const makeALink = function(table, id, uri, text) {
if(!["navbar_items", "sources"].includes(table)) {
return "<a href='/" + getEditPageFromTable(table) + ".html?uri=" +
uri + "'><div>" + escapeHTML(text) + "</div></a>";
} else if (id === "sources-table") {
return "<a source-uri='" + uri + "' href='#source-modal'><div source-uri='" + uri + "'>" + escapeHTML(text) + "</div></a>"
}
return "<div>" + escapeHTML(text) + "</div>";
}
const getTdClass = function(id, column) {
let tdClass = "";
const lines = 2;
let titleSize = 16 * lines;
if (mobileScreen() && id === 'modal-references-table') {
tdClass = "mobile-td-in-modal";
} else if (mobileScreen()) {
tdClass = "mobile-td";
} else if (smallTd(id, column)) {
tdClass = "small-td";
titleSize = 10 * lines;
} else if (modestTd(id, column)) {
tdClass = "modest-td";
titleSize = 13 * lines;
} else if (bigTd(id, column)) {
tdClass = "big-td";
titleSize = 33 * lines;
} else {
tdClass = "normal-td";
}
if (["Title", "Author", "Label", "Alt Text", "Name", "Location"].includes(column)) {
tdClass += " hyphen-td";
}
return [tdClass, titleSize];
}
const generateTrStr = function(id, table, row, uri, columns, counter, args){
let tr = "<tr id='tr-" + counter + "' style='display:run-in'><td class='checkbox-td'><input type='checkbox' id='checkbox-"
+ counter + "' value='" + uri + "'></td>";
for (let i = 0; i < args.length; i++) {
const [tdClass, titleSize] = getTdClass(id, args[i][1]);
if (["media-table"].includes(id) && args[i][1] === thumbnail_column) {
tr += "<td id='thumbnail-" + i + "' uri='" + uri + "' class='thumbnail-td' name='" + args[i][1] + "'><div> </div></td>";
} else {
tr += "<td class='" + tdClass + "' name='" + args[i][1] + "' title='" + titleIfBig(row[columns[i]], titleSize) +
"'>" + makeALink(table, id, uri, row[columns[i]]) + "</td>";
}
}
tr += "</tr>";
return tr;
}
const setCheckboxHandler = function(){
const thCheckbox = globalThis.document.getElementById("th-checkbox");
if (thCheckbox) {
thCheckbox.addEventListener("change", thCheckboxHandler);
}
}
const addOrderByIcons = function(dataTable, db, id, table, ...args) {
let ths = dataTable.getElementsByTagName("th");
ths = [].slice.call(ths).slice(1);
for (let i = 0; i < ths.length; i++) {
if (args[i][1] === thumbnail_column) continue;
const div = ths[i].getElementsByTagName("div")[0];
div.innerHTML = args[i][1] + " <span id='th-" + i + "'><img class='data-table-icon' src='images/chevron-down.svg' /></span>";
const span = globalThis.document.getElementById("th-" + i);
span.addEventListener("click", async (event) => {
let newSpan;
const src = event.target.attributes["src"];
if (src && src.value && src.value.includes('chevron-down.svg')) {
await fillDOM(db, id, table, args[i][0] + " ASC", ...args);
newSpan = globalThis.document.getElementById("th-" + i);
newSpan.innerHTML = "<img class='data-table-icon' src='images/chevron-up.svg' />";
} else {
await fillDOM(db, id, table, args[i][0] + " DESC", ...args);
newSpan = globalThis.document.getElementById("th-" + i);
newSpan.innerHTML = "<img class='data-table-icon' src='images/chevron-down.svg' />";
}
if (newSpan) newSpan.style = "filter: brightness(0) saturate(100%) invert(7%) sepia(95%) saturate(6909%) hue-rotate(247deg) brightness(128%) contrast(146%);";
if (["media-table"].includes(id)) addThumbnails();
});
}
}
const preFormatThead = function() {
let page = thisPage();
let header;
let body;
let id;
switch (page) {
case "index":
break;
case "articles":
id = "articles-table";
if (smallScreen()) {
header = generateHeaderStr(id, [["", "ID"], ["", "Type"], ["", "Status"], ["", "Title"], ["", "Author(s)"], ["", "Section"], ["", "Series"], ["", "Created"]]);
body = "<td><div> </div></td>".repeat(9);
} else {
header = generateHeaderStr(id, [["", "ID"], ["", "Type"], ["", "Status"], ["", "Title"], ["", "Author(s)"], ["", "Section"], ["", "Series"], ["", "Created"], ["", "Updated"]]);
}
break;
case "series":
id = "series-table";
header = generateHeaderStr(id, [["", "ID"], ["", "Status"], ["", "Section"], ["", "Title"], ["", "Author(s)"], ["", "Created"], ["", "Updated"]]);
break;
case "sections":
id = "sections-table";
header = generateHeaderStr(id, [["", "ID"], ["", "Title"], ["", "Author(s)"], ["", "Created"], ["", "Updated"]]);
break;
case "relations":
id = "relations-table";
header = generateHeaderStr(id, [["", "ID"], ["", "Article 1"], ["", "Type"], ["", "Article 2"], ["", "Place"], ["", "Author(s)"], ["", "Created"], ["", "Updated"]]);
break;
case "sources":
id = "sources-table";
header = generateHeaderStr(id, [["", "ID"], ["", "Type"], ["", "Author"], ["", "Year"], ["", "Title"]]);
break;
case "article-edit":
id = "modal-references-table";
header = generateHeaderStr(id, [["", "ID"], ["", "Author"], ["", "Title"], ["", "Type"], ["", "Year"]]);
break;
case "navbar":
id = "navbar-table";
header = generateHeaderStr(id, [["", "ID"], ["", "Label"], ["", "Location"], ["", "Type of Ref."], ["", "ID or URL"], ["", "Created"]]);
break;
case "media":
id = "media-table";
header = generateHeaderStr(id, [["", "ID"], ["", "File Ext."], ["", "Alt Text"], ["", "Size (bytes)"], ["", "Created"], ["", "Updated"], ["", thumbnail_column]]);
break;
case "authors":
id = "authors-table";
header = generateHeaderStr(id, [["", "ID"], ["", "Name"], ["", "Email"], ["", "Location"], ["", "Created"], ["", "Updated"]]);
break;
default:
}
if (id) {
const dataTable = globalThis.document.getElementById(id);
const thead = dataTable.getElementsByTagName('thead')[0];
thead.innerHTML = header;
if (body) {
const tbody = dataTable.getElementsByTagName('tbody')[0];
tbody.innerHTML = body;
const tfoot = dataTable.getElementsByTagName('tfoot')[0];
tfoot.innerHTML = tfoot.innerHTML.replace("colspan=\"5\" id=\"tfoot-right-td\"", "colspan=\"4\" id=\"tfoot-right-td\"");
}
}
}
const fillDOM = async function(db, id, table, orderBy, ...args){
let [header, columns] = generateHeader(id, args);
const dataTable = globalThis.document.getElementById(id);
const thead = dataTable.getElementsByTagName('thead')[0];
const tbody = dataTable.getElementsByTagName('tbody')[0];
thead.innerHTML = header;
const uriField = table === 'sources' ? 'ris_id' : 'uri';
const completeCols = columns.slice(0);
if (!completeCols.includes(uriField)) completeCols.push(uriField);
let resultRows = await db.exec({
sql: "SELECT " + completeCols.join(',') + " FROM " + table + (orderBy ? " ORDER BY " + orderBy : ""),
rowMode: 'object'
});
tbody.innerHTML = "";
for (let i = 0, counter = 0; i < resultRows.length; i++) {
let row = resultRows[i];
tbody.innerHTML += generateTrStr(id, table, row, row[uriField], columns, counter++, args);
}
setCheckboxHandler();
addOrderByIcons(thead, db, id, table, ...args);
if (tbody.rows.length === 0) {
addEmptyTr();
}
setEntriesPerPage();
}
const fetchData = function(db){
let page = thisPage();
if (mobileScreen()) {
switch (page) {
case "index":
break;
case "articles":
fillDOM(db, "articles-table", "articles", "created ASC", ["uri", "ID"], ["title", "Title"]);
break;
case "series":
fillDOM(db, "series-table", "series", "created ASC", ["uri", "ID"], ["title", "Title"]);
break;
case "sections":
fillDOM(db, "sections-table", "sections", "created ASC", ["uri", "ID"], ["title", "Title"]);
break;
case "relations":
fillDOM(db, "relations-table", "relations", "created ASC", ["article1", "Article 1"], ["article2", "Article 2"]);
break;
case "sources":
fillDOM(db, "sources-table", "sources", "", ["IIF(ris_a1 = '', ris_au, ris_a1)", "Author"], ["IIF(ris_t1 = '', IIF(ris_ti = '', ris_st, ris_ti), ris_t1)", "Title"]);
break;
case "article-edit":
fillDOM(db, "modal-references-table", "sources", "", ["IIF(ris_a1 = '', ris_au, ris_a1)", "Author"], ["IIF(ris_t1 = '', IIF(ris_ti = '', ris_st, ris_ti), ris_t1)", "Title"]);
break;
case "navbar":
fillDOM(db, "navbar-table", "navbar_items", "created ASC", ["uri", "ID"], ["label", "Label"]);
break;
case "media":
fillDOM(db, "media-table", "media", "created ASC", ["uri", "ID"], ["''", thumbnail_column])
.then(addThumbnails);
break;
case "authors":
fillDOM(db, "authors-table", "authors", "created ASC", ["uri", "ID"], ["name", "Name"]);
break;
default:
}
} else {
switch (page) {
case "index":
break;
case "articles":
if (smallScreen()) {
fillDOM(db, "articles-table", "articles", "created ASC", ["uri", "ID"], ["type", "Type"],
["status", "Status"], ["title", "Title"], ["authors_ids", "Author(s)"], ["section_uri", "Section"],
["series_uri", "Series"], ["created", "Created"]);
} else {
fillDOM(db, "articles-table", "articles", "created ASC", ["uri", "ID"], ["type", "Type"],
["status", "Status"], ["title", "Title"], ["authors_ids", "Author(s)"], ["section_uri", "Section"],
["series_uri", "Series"], ["created", "Created"], ["updated", "Updated"]);
}
break;
case "series":
fillDOM(db, "series-table", "series", "created ASC", ["uri", "ID"], ["status", "Status"], ["section_uri", "Section"],
["title", "Title"], ["authors_ids", "Author(s)"], ["created", "Created"], ["updated", "Updated"]);
break;
case "sections":
fillDOM(db, "sections-table", "sections", "created ASC", ["uri", "ID"], ["title", "Title"],
["authors_ids", "Author(s)"], ["created", "Created"], ["updated", "Updated"]);
break;
case "relations":
fillDOM(db, "relations-table", "relations", "created ASC", ["uri", "ID"], ["article1", "Article 1"],
["type", "Type"], ["article2", "Article 2"], ["place", "Place"], ["authors_ids", "Author(s)"],
["created", "Created"], ["updated", "Updated"]);
break;
case "sources":
fillDOM(db, "sources-table", "sources", "", ["ris_id", "ID"], ["ris_ty", "Type"],
["IIF(ris_a1 = '', ris_au, ris_a1)", "Author"], ["IIF(ris_y1 = '', ris_py, ris_y1)", "Year"],
["IIF(ris_t1 = '', IIF(ris_ti = '', ris_st, ris_ti), ris_t1)", "Title"]);
break;
case "article-edit":
fillDOM(db, "modal-references-table", "sources", "", ["ris_id", "ID"],
["IIF(ris_a1 = '', ris_au, ris_a1)", "Author"], ["IIF(ris_t1 = '', IIF(ris_ti = '', ris_st, ris_ti), ris_t1)", "Title"],
["ris_ty", "Type"], ["IIF(ris_y1 = '', ris_py, ris_y1)", "Year"]);
break;
case "navbar":
fillDOM(db, "navbar-table", "navbar_items", "created ASC", ["uri", "ID"], ["label", "Label"],
["location", "Location"], ["type", "Type of Ref."], ["reference", "ID or URL"], ["created", "Created"]);
break;
case "media":
fillDOM(db, "media-table", "media", "created ASC", ["uri", "ID"], ["file_extension", "File Ext."], ["alt_text", "Alt Text"],
["size", "Size (bytes)"], ["created", "Created"], ["updated", "Updated"], ["''", thumbnail_column])
.then(addThumbnails);
break;
case "authors":
fillDOM(db, "authors-table", "authors", "created ASC", ["uri", "ID"], ["name", "Name"],
["email", "Email"], ["location", "Location"], ["created", "Created"], ["updated", "Updated"]);
break;
default:
}
}
}
const addThumbnails = async function() {
if (!globalThis.mediaUrls) {
globalThis.mediaUrls = {};
const response = await globalThis.lb.listMediaFiles();
if (response.type !== "error") {
response.result.forEach((item) => {
globalThis.mediaUrls[item['name']] = item['url'];
});
}
}
const tdsCollection = globalThis.document.getElementsByClassName("thumbnail-td");
const tds = Array.from(tdsCollection);
tds.forEach((td) => {
td.innerHTML = "<img class='thumbnail-img' src='" + globalThis.mediaUrls[td.attributes['uri'].value] + "' />";
});
}
const reloadPage = function() {
let href = globalThis.location.href;
const hashtagPos = href.indexOf("#");
if (hashtagPos !== -1) href = href.substring(0, hashtagPos);
globalThis.location.href = href;
}
const deleteItems = function(items, sql) {
let itemsStr = "'" + items.join("','") + "'";
globalThis.lb.useSqlite(async (db) => {
await db.exec({
sql: sql(itemsStr)
});
reloadPage();
});
}
const deleteArticles = function(items) {
deleteItems(items, (a) => "DELETE FROM articles WHERE uri IN (" + a + ");" +
"DELETE FROM relations WHERE article1 IN (" + a + ") OR article2 IN (" + a + ");");
}
const deleteSeries = function(items){
deleteItems(items, (a) => "DELETE FROM series WHERE uri IN (" + a + ");" +
"UPDATE articles SET series_uri='' WHERE series_uri IN (" + a + ");");
}
const deleteSections = function(items){
deleteItems(items, (a) => "DELETE FROM sections WHERE uri IN (" + a + ");" +
"UPDATE articles SET section_uri='' WHERE section_uri IN (" + a + ");");
}
const deleteRelations = function(items){
deleteItems(items, (a) => "DELETE FROM relations WHERE uri IN (" + a + ");");
}
const deleteSources = function(items){
deleteItems(items, (a) => "DELETE FROM sources WHERE ris_id IN (" + a + ");");
}
const deleteNavbarItems = function(items){
deleteItems(items, (a) => "DELETE FROM navbar_items WHERE uri IN (" + a + ");");
}
const deleteMediaFiles = function(items) {
let itemsStr = "'" + items.join("','") + "'";
globalThis.lb.useSqlite(async (db) => {
await db.exec({
sql: "DELETE FROM media WHERE uri IN (" + itemsStr + ");"
});
for (let i = 0; i < items.length; i++) {
await deleteMediaFileAndVersions(items[i]);
}
reloadPage();
});
}
const deleteAuthors = function(items){
deleteItems(items, (a) => "DELETE FROM authors WHERE uri IN (" + a + ");");
}
const changeStatus = function(table, items, status){
let itemsStr = "'" + items.join("','") + "'";
globalThis.lb.useSqlite(async (db) => {
await db.exec({
sql: "UPDATE " + table + " SET status='" + status + "' WHERE uri IN (" + itemsStr + ");"
});
reloadPage();
});
}
const thisTable = function(){
switch (thisPage()) {
case "articles":
return "articles";
case "series":
return "series";
case "sections":
return "sections";
case "relations":
return "relations";
case "sources":
return "sources";
case "navbar":
return "navbar_items";
case "media":
return "media";
case "authors":
return "authors";
default:
return "";
}
}
const actionFormHandler = function(event) {
event.preventDefault();
const selectAction = globalThis.document.getElementById("select-action");
const action = selectAction.value;
if (action === "none"){
event.preventDefault();
return;
}
let selectedItems = [];
const checkboxes = getAllVisibleCheckboxes();
for (let checkbox in checkboxes) {
if (checkboxes[checkbox].checked) {
selectedItems.push(checkboxes[checkbox].value);
}
}
if (selectedItems.length === 0){
event.preventDefault();
return;
}
if (action === "delete"){
switch (thisPage()) {
case "articles":
deleteArticles(selectedItems);
break;
case "series":
deleteSeries(selectedItems);
break;
case "sections":
deleteSections(selectedItems);
break;
case "relations":
deleteRelations(selectedItems);
break;
case "sources":
deleteSources(selectedItems);
break;
case "navbar":
deleteNavbarItems(selectedItems);
break;
case "media":
deleteMediaFiles(selectedItems);
break;
case "authors":
deleteAuthors(selectedItems);
break;
default:
}
} else if (action === "publish" || action === "unpublish") {
changeStatus(thisTable(), selectedItems, action === "publish" ? "Published" : "Unpublished");
}
}
const strFromSelectValues = function(select) {
let result = "";
for (let i = 0, len = select.options.length; i < len; i++) {
const option = select.options[i];
if (option.selected) {
result += option.value + ",";
}
}
if (result.length > 0) return result.slice(0, -1);
return result;
}
const insertOrUpdateRecord = async function(db, table, columns, fieldValues) {
const columnsStr = columns.join(", ");
//Using UPSERT in order not to update the field 'created'
let sqlStr = "INSERT INTO " + table + "(" + columnsStr + ", created, updated) VALUES " +
"(" + "?, ".repeat(fieldValues.length) + "STRFTIME('%Y-%m-%d %H:%M', DATETIME('now','localtime')), " +
"STRFTIME('%Y-%m-%d %H:%M', DATETIME('now','localtime'))) ON CONFLICT(uri) DO UPDATE SET "
+ columns.join("=?, ") + "=?, updated=STRFTIME('%Y-%m-%d %H:%M', DATETIME('now','localtime'));";
await db.exec({
sql: sqlStr,
bind: [...fieldValues, ...fieldValues]
}, {if_error_do: (e) => globalThis.window.alert("Error: " + e)});
}
const formHandler = function(table, fields){
const fieldValues = [];
let columns = [];
let uri = "";
for (let i = 0; i < fields.length; i++) {
columns[i] = fields[i][1];
switch (fields[i][2]) {
case "select":
const select = globalThis.document.getElementById(fields[i][0]);
fieldValues[i] = select.options[select.selectedIndex].value;
break;
case "fix-datetime-format":
let value = globalThis.document.getElementById(fields[i][0]).value;
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value)) value.replace("T", " ");
fieldValues[i] = value;
break;
case "checkbox":
fieldValues[i] = globalThis.document.getElementById(fields[i][0]).checked ? 1 : 0;
break;
case "select-multiple":
fieldValues[i] = strFromSelectValues(globalThis.document.getElementById(fields[i][0]));
break;
default:
fieldValues[i] = globalThis.document.getElementById(fields[i][0]).value;
}
if (fields[i][1] === 'uri') uri = fieldValues[i];
}
const textarea = globalThis.document.getElementById("editor");
if (textarea) {
const contents = globalThis.thisEditor.getValue();
fieldValues.push(contents);
columns.push("contents");
}
globalThis.lb.useSqlite(async (db) => {
await insertOrUpdateRecord(db, table, columns, fieldValues);
if (table === "relations") {
uri = await getRelationURI(fields, fieldValues, db);
} else if (table === "media") {
const versions = getVersions(columns, fieldValues);
await saveFileInMediaFolder(versions);
}
if (findParameter(globalThis.location.href, "uri") === uri
|| table === "navbar_items" || table === "templates") {
reloadPage();
} else {
addURItoURL(uri);
}
});
}
const getVersions = function(columns, fieldValues) {
let versions = [];
for (let i = 0; i < columns.length; i++) {
if (columns[i] === 'versions') {
versions = fieldValues[i].split(",");
}
}
return versions;
}
const addURItoURL = function(uri) {
globalThis.location.href = globalThis.location.href.split("?uri=")[0] + "?uri=" + uri;
}
const getRelationURI = async function(fields, fieldValues, db) {
let article1, article2, type = "";
for (let i = 0; i < fields.length; i++) {
if (fields[i][1] === 'article1') {
article1 = fieldValues[i];
} else if(fields[i][1] === 'article2') {
article2 = fieldValues[i];
} else if(fields[i][1] === 'type') {
type = fieldValues[i];
}
}
let resultRows = await db.exec({
sql: "SELECT uri FROM relations WHERE article1 = '" + article1 +
"' AND article2 = '" + article2 + "' AND type = '" + type + "'",
rowMode: 'object'
});
if (resultRows && resultRows[0]) return resultRows[0]["uri"];
return "";
}
const articleFormHandler = function(event) {
event.preventDefault();
formHandler("articles", [["uri", "uri"], ["type", "type", "select"], ["section-uri", "section_uri", "select"],
["series-uri", "series_uri", "select"], ["highlight-mainpage", "highlight_mainpage", "checkbox"],
["highlight-section", "highlight_section", "checkbox"], ["in-sitemap", "in_sitemap", "checkbox"],
["title", "title"], ["notes", "notes"], ["summary", "summary"], ["geolocation", "geolocation"],
["authors", "authors_ids", "select-multiple"], ["subtitle", "subtitle"], ["label", "label"],
["keywords", "keywords"], ["photo", "photo"], ["photo-info", "photo_info"], ["language", "language"],
["enable-comments", "enable_comments", "checkbox"], ["video", "video"], ["video-info", "video_info"], ["audio", "audio"],
["audio-info", "audio_info"], ["editorial-notes", "editorial_notes"], ["published", "published", "fix-datetime-format"],
["status", "status", "select"]]);
}
const seriesFormHandler = function(event) {
event.preventDefault();
formHandler("series", [["uri", "uri"], ["type", "type", "select"], ["highlight-mainpage", "highlight_mainpage", "checkbox"],
["highlight-section", "highlight_section", "checkbox"], ["in-sitemap", "in_sitemap", "checkbox"], ["title", "title"],
["authors", "authors_ids", "select-multiple"], ["subtitle", "subtitle"], ["photo", "photo"],
["photo-info", "photo_info"], ["keywords", "keywords"], ["published", "published", "fix-datetime-format"],
["status", "status", "select"], ["section-uri", "section_uri", "select"]]);
}
const sectionFormHandler = function(event) {
event.preventDefault();
formHandler("sections", [["uri", "uri"], ["type", "type", "select"], ["title", "title"], ["description", "description"],
["keywords", "keywords"], ["authors", "authors_ids", "select-multiple"]]);
}
const relationFormHandler = function(event) {
event.preventDefault();
const uriInput = globalThis.document.getElementById("uri");
if (!uriInput.value) uriInput.value = globalThis.lb.generateRandomId();
formHandler("relations", [["uri", "uri"], ["article1", "article1", "select"],
["article2", "article2", "select"], ["relation-type", "type", "select"], ["place", "place", "fix-datetime-format"],
["title", "title"], ["photo", "photo"], ["photo-info", "photo_info"], ["authors", "authors_ids", "select-multiple"]]);
}
const authorFormHandler = function(event) {
event.preventDefault();
formHandler("authors", [["uri", "uri"], ["type", "type", "select"], ["name", "name"], ["contact", "contact"],
["location", "l