5148 lines
268 KiB
HTML
5148 lines
268 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Content Collaboration</title>
|
|
<script src="scripts/components.js" defer></script>
|
|
<link rel="stylesheet" href="css/main.min.css">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Calistoga&family=Roboto:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
|
|
rel="stylesheet">
|
|
<script src="scripts/purify.min.js" defer></script>
|
|
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.4.6" defer></script>
|
|
<script id="floGlobals">
|
|
/* Constants for FLO blockchain operations !!Make sure to add this at beginning!! */
|
|
const floGlobals = {
|
|
|
|
//Required for all
|
|
blockchain: "FLO",
|
|
|
|
//Required for blockchain API operators
|
|
apiURL: {
|
|
FLO: ['https://flosight.duckdns.org/', 'https://livenet.flocha.in/'],
|
|
FLO_TEST: ['https://testnet-flosight.duckdns.org/', 'https://testnet.flocha.in/']
|
|
},
|
|
adminID: "FC18nZVcHifauCsRMMeH4gkGxexuhSMFgs",
|
|
sendAmt: 0.001,
|
|
fee: 0.0005,
|
|
|
|
//Required for Supernode operations
|
|
SNStorageID: "FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk",
|
|
supernodes: {}, //each supernode must be stored as floID : {uri:<uri>,pubKey:<publicKey>}
|
|
|
|
//for cloud apps
|
|
subAdmins: [],
|
|
application: "CC",
|
|
appObjects: {},
|
|
generalData: {},
|
|
lastVC: {}
|
|
}
|
|
</script>
|
|
</head>
|
|
|
|
<body class="hide" onload="onLoadStartUp()">
|
|
<sm-notifications id="notification_drawer"></sm-notifications>
|
|
<sm-popup id="confirmation_popup">
|
|
<h4 id="confirm_title"></h4>
|
|
<p id="confirm_message"></p>
|
|
<div class="flex align-center">
|
|
<sm-button variant="no-outline" class="cancel-btn">Cancel</sm-button>
|
|
<sm-button variant="no-outline" class="submit-btn">OK</sm-button>
|
|
</div>
|
|
</sm-popup>
|
|
<article id="landing" class="page page-layout hide-completely">
|
|
<header class="flex space-between">
|
|
<div class="logo">
|
|
<svg class="main-logo" viewBox="0 0 27.25 32">
|
|
<title>RanchiMall</title>
|
|
<path
|
|
d="M27.14,30.86c-.74-2.48-3-4.36-8.25-6.94a20,20,0,0,1-4.2-2.49,6,6,0,0,1-1.25-1.67,4,4,0,0,1,0-2.26c.37-1.08.79-1.57,3.89-4.55a11.66,11.66,0,0,0,3.34-4.67,6.54,6.54,0,0,0,.05-2.82C20,3.6,18.58,2,16.16.49c-.89-.56-1.29-.64-1.3-.24a3,3,0,0,1-.3.72l-.3.55L13.42.94C13,.62,12.4.26,12.19.15c-.4-.2-.73-.18-.72.05a9.39,9.39,0,0,1-.61,1.33s-.14,0-.27-.13C8.76.09,8-.27,8,.23A11.73,11.73,0,0,1,6.76,2.6C4.81,5.87,2.83,7.49.77,7.49c-.89,0-.88,0-.61,1,.22.85.33.92,1.09.69A5.29,5.29,0,0,0,3,8.33c.23-.17.45-.29.49-.26a2,2,0,0,1,.22.63A1.31,1.31,0,0,0,4,9.34a5.62,5.62,0,0,0,2.27-.87L7,8l.13.55c.19.74.32.82,1,.65a7.06,7.06,0,0,0,3.46-2.47l.6-.71-.06.64c-.17,1.63-1.3,3.42-3.39,5.42L6.73,14c-3.21,3.06-3,5.59.6,8a46.77,46.77,0,0,0,4.6,2.41c.28.13,1,.52,1.59.87,3.31,2,4.95,3.92,4.95,5.93a2.49,2.49,0,0,0,.07.77h0c.09.09,0,.1.9-.14a2.61,2.61,0,0,0,.83-.32,3.69,3.69,0,0,0-.55-1.83A11.14,11.14,0,0,0,17,26.81a35.7,35.7,0,0,0-5.1-2.91C9.37,22.64,8.38,22,7.52,21.17a3.53,3.53,0,0,1-1.18-2.48c0-1.38.71-2.58,2.5-4.23,2.84-2.6,3.92-3.91,4.67-5.65a3.64,3.64,0,0,0,.42-2A3.37,3.37,0,0,0,13.61,5l-.32-.74.29-.48c.17-.27.37-.63.46-.8l.15-.3.44.64a5.92,5.92,0,0,1,1,2.81,5.86,5.86,0,0,1-.42,1.94c0,.12-.12.3-.15.4a9.49,9.49,0,0,1-.67,1.1,28,28,0,0,1-4,4.29C8.62,15.49,8.05,16.44,8,17.78a3.28,3.28,0,0,0,1.11,2.76c.95,1,2.07,1.74,5.25,3.32,3.64,1.82,5.22,2.9,6.41,4.38A4.78,4.78,0,0,1,21.94,31a3.21,3.21,0,0,0,.14.92,1.06,1.06,0,0,0,.43-.05l.83-.22.46-.12-.06-.46c-.21-1.53-1.62-3.25-3.94-4.8a37.57,37.57,0,0,0-5.22-2.82A13.36,13.36,0,0,1,11,21.19a3.36,3.36,0,0,1-.8-4.19c.41-.85.83-1.31,3.77-4.15,2.39-2.31,3.43-4.13,3.43-6a5.85,5.85,0,0,0-2.08-4.29c-.23-.21-.44-.43-.65-.65A2.5,2.5,0,0,1,15.27.69a10.6,10.6,0,0,1,2.91,2.78A4.16,4.16,0,0,1,19,6.16a4.91,4.91,0,0,1-.87,3c-.71,1.22-1.26,1.82-4.27,4.67a9.47,9.47,0,0,0-2.07,2.6,2.76,2.76,0,0,0-.33,1.54,2.76,2.76,0,0,0,.29,1.47c.57,1.21,2.23,2.55,4.65,3.73a32.41,32.41,0,0,1,5.82,3.24c2.16,1.6,3.2,3.16,3.2,4.8a1.94,1.94,0,0,0,.09.76,4.54,4.54,0,0,0,1.66-.4C27.29,31.42,27.29,31.37,27.14,30.86ZM6.1,7h0a3.77,3.77,0,0,1-1.46.45L4,7.51l.68-.83a25.09,25.09,0,0,0,3-4.82A12,12,0,0,1,8.28.76c.11-.12.77.32,1.53,1l.63.58-.57.84A10.34,10.34,0,0,1,6.1,7Zm5.71-1.78A9.77,9.77,0,0,1,9.24,7.18h0a5.25,5.25,0,0,1-1.17.28l-.58,0,.65-.78a21.29,21.29,0,0,0,2.1-3.12c.22-.41.42-.76.44-.79s.5.43.9,1.24L12,5ZM13.41,3a2.84,2.84,0,0,1-.45.64,11,11,0,0,1-.9-.91l-.84-.9.19-.45c.34-.79.39-.8,1-.31A9.4,9.4,0,0,1,13.8,2.33q-.18.34-.39.69Z" />
|
|
</svg>
|
|
<div class="grid">
|
|
<h4>RanchiMall CC</h4>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-0-5">
|
|
<a href="#/sign_up" class="button">Sign up</a>
|
|
<a href="#/sign_in" class="button button--primary">Sign in</a>
|
|
</div>
|
|
</header>
|
|
<section class="grid justify-center">
|
|
<div class="grid gap-0-5">
|
|
<h1 class="h1">Create. Collaborate. <br>Publish.</h1>
|
|
<p>Write something great... <strong>Together</strong>.</p>
|
|
</div>
|
|
</section>
|
|
</article>
|
|
<article id="sign_in" class="page page-layout hide-completely">
|
|
<header>
|
|
<div class="logo">
|
|
<svg class="main-logo" viewBox="0 0 27.25 32">
|
|
<title>RanchiMall</title>
|
|
<path
|
|
d="M27.14,30.86c-.74-2.48-3-4.36-8.25-6.94a20,20,0,0,1-4.2-2.49,6,6,0,0,1-1.25-1.67,4,4,0,0,1,0-2.26c.37-1.08.79-1.57,3.89-4.55a11.66,11.66,0,0,0,3.34-4.67,6.54,6.54,0,0,0,.05-2.82C20,3.6,18.58,2,16.16.49c-.89-.56-1.29-.64-1.3-.24a3,3,0,0,1-.3.72l-.3.55L13.42.94C13,.62,12.4.26,12.19.15c-.4-.2-.73-.18-.72.05a9.39,9.39,0,0,1-.61,1.33s-.14,0-.27-.13C8.76.09,8-.27,8,.23A11.73,11.73,0,0,1,6.76,2.6C4.81,5.87,2.83,7.49.77,7.49c-.89,0-.88,0-.61,1,.22.85.33.92,1.09.69A5.29,5.29,0,0,0,3,8.33c.23-.17.45-.29.49-.26a2,2,0,0,1,.22.63A1.31,1.31,0,0,0,4,9.34a5.62,5.62,0,0,0,2.27-.87L7,8l.13.55c.19.74.32.82,1,.65a7.06,7.06,0,0,0,3.46-2.47l.6-.71-.06.64c-.17,1.63-1.3,3.42-3.39,5.42L6.73,14c-3.21,3.06-3,5.59.6,8a46.77,46.77,0,0,0,4.6,2.41c.28.13,1,.52,1.59.87,3.31,2,4.95,3.92,4.95,5.93a2.49,2.49,0,0,0,.07.77h0c.09.09,0,.1.9-.14a2.61,2.61,0,0,0,.83-.32,3.69,3.69,0,0,0-.55-1.83A11.14,11.14,0,0,0,17,26.81a35.7,35.7,0,0,0-5.1-2.91C9.37,22.64,8.38,22,7.52,21.17a3.53,3.53,0,0,1-1.18-2.48c0-1.38.71-2.58,2.5-4.23,2.84-2.6,3.92-3.91,4.67-5.65a3.64,3.64,0,0,0,.42-2A3.37,3.37,0,0,0,13.61,5l-.32-.74.29-.48c.17-.27.37-.63.46-.8l.15-.3.44.64a5.92,5.92,0,0,1,1,2.81,5.86,5.86,0,0,1-.42,1.94c0,.12-.12.3-.15.4a9.49,9.49,0,0,1-.67,1.1,28,28,0,0,1-4,4.29C8.62,15.49,8.05,16.44,8,17.78a3.28,3.28,0,0,0,1.11,2.76c.95,1,2.07,1.74,5.25,3.32,3.64,1.82,5.22,2.9,6.41,4.38A4.78,4.78,0,0,1,21.94,31a3.21,3.21,0,0,0,.14.92,1.06,1.06,0,0,0,.43-.05l.83-.22.46-.12-.06-.46c-.21-1.53-1.62-3.25-3.94-4.8a37.57,37.57,0,0,0-5.22-2.82A13.36,13.36,0,0,1,11,21.19a3.36,3.36,0,0,1-.8-4.19c.41-.85.83-1.31,3.77-4.15,2.39-2.31,3.43-4.13,3.43-6a5.85,5.85,0,0,0-2.08-4.29c-.23-.21-.44-.43-.65-.65A2.5,2.5,0,0,1,15.27.69a10.6,10.6,0,0,1,2.91,2.78A4.16,4.16,0,0,1,19,6.16a4.91,4.91,0,0,1-.87,3c-.71,1.22-1.26,1.82-4.27,4.67a9.47,9.47,0,0,0-2.07,2.6,2.76,2.76,0,0,0-.33,1.54,2.76,2.76,0,0,0,.29,1.47c.57,1.21,2.23,2.55,4.65,3.73a32.41,32.41,0,0,1,5.82,3.24c2.16,1.6,3.2,3.16,3.2,4.8a1.94,1.94,0,0,0,.09.76,4.54,4.54,0,0,0,1.66-.4C27.29,31.42,27.29,31.37,27.14,30.86ZM6.1,7h0a3.77,3.77,0,0,1-1.46.45L4,7.51l.68-.83a25.09,25.09,0,0,0,3-4.82A12,12,0,0,1,8.28.76c.11-.12.77.32,1.53,1l.63.58-.57.84A10.34,10.34,0,0,1,6.1,7Zm5.71-1.78A9.77,9.77,0,0,1,9.24,7.18h0a5.25,5.25,0,0,1-1.17.28l-.58,0,.65-.78a21.29,21.29,0,0,0,2.1-3.12c.22-.41.42-.76.44-.79s.5.43.9,1.24L12,5ZM13.41,3a2.84,2.84,0,0,1-.45.64,11,11,0,0,1-.9-.91l-.84-.9.19-.45c.34-.79.39-.8,1-.31A9.4,9.4,0,0,1,13.8,2.33q-.18.34-.39.69Z" />
|
|
</svg>
|
|
<div class="grid">
|
|
<h4>RanchiMall CC</h4>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<section>
|
|
<h1 class="h2">Sign In</h1>
|
|
<p>Welcome back, glad to see you again</p>
|
|
<sm-form>
|
|
<sm-input id="private_key_field" type="password" placeholder="FLO private key"
|
|
error-text="Private key is invalid" data-private-key required></sm-input>
|
|
<sm-button id="sign_in_button" variant="primary" disabled>Sign In</sm-button>
|
|
</sm-form>
|
|
<p>
|
|
New here? <a href="#/sign_up">get your FLO login credentials</a>
|
|
</p>
|
|
</section>
|
|
</article>
|
|
<article id="sign_up" class="page page-layout hide-completely">
|
|
<header>
|
|
<div class="logo">
|
|
<svg class="main-logo" viewBox="0 0 27.25 32">
|
|
<title>RanchiMall</title>
|
|
<path
|
|
d="M27.14,30.86c-.74-2.48-3-4.36-8.25-6.94a20,20,0,0,1-4.2-2.49,6,6,0,0,1-1.25-1.67,4,4,0,0,1,0-2.26c.37-1.08.79-1.57,3.89-4.55a11.66,11.66,0,0,0,3.34-4.67,6.54,6.54,0,0,0,.05-2.82C20,3.6,18.58,2,16.16.49c-.89-.56-1.29-.64-1.3-.24a3,3,0,0,1-.3.72l-.3.55L13.42.94C13,.62,12.4.26,12.19.15c-.4-.2-.73-.18-.72.05a9.39,9.39,0,0,1-.61,1.33s-.14,0-.27-.13C8.76.09,8-.27,8,.23A11.73,11.73,0,0,1,6.76,2.6C4.81,5.87,2.83,7.49.77,7.49c-.89,0-.88,0-.61,1,.22.85.33.92,1.09.69A5.29,5.29,0,0,0,3,8.33c.23-.17.45-.29.49-.26a2,2,0,0,1,.22.63A1.31,1.31,0,0,0,4,9.34a5.62,5.62,0,0,0,2.27-.87L7,8l.13.55c.19.74.32.82,1,.65a7.06,7.06,0,0,0,3.46-2.47l.6-.71-.06.64c-.17,1.63-1.3,3.42-3.39,5.42L6.73,14c-3.21,3.06-3,5.59.6,8a46.77,46.77,0,0,0,4.6,2.41c.28.13,1,.52,1.59.87,3.31,2,4.95,3.92,4.95,5.93a2.49,2.49,0,0,0,.07.77h0c.09.09,0,.1.9-.14a2.61,2.61,0,0,0,.83-.32,3.69,3.69,0,0,0-.55-1.83A11.14,11.14,0,0,0,17,26.81a35.7,35.7,0,0,0-5.1-2.91C9.37,22.64,8.38,22,7.52,21.17a3.53,3.53,0,0,1-1.18-2.48c0-1.38.71-2.58,2.5-4.23,2.84-2.6,3.92-3.91,4.67-5.65a3.64,3.64,0,0,0,.42-2A3.37,3.37,0,0,0,13.61,5l-.32-.74.29-.48c.17-.27.37-.63.46-.8l.15-.3.44.64a5.92,5.92,0,0,1,1,2.81,5.86,5.86,0,0,1-.42,1.94c0,.12-.12.3-.15.4a9.49,9.49,0,0,1-.67,1.1,28,28,0,0,1-4,4.29C8.62,15.49,8.05,16.44,8,17.78a3.28,3.28,0,0,0,1.11,2.76c.95,1,2.07,1.74,5.25,3.32,3.64,1.82,5.22,2.9,6.41,4.38A4.78,4.78,0,0,1,21.94,31a3.21,3.21,0,0,0,.14.92,1.06,1.06,0,0,0,.43-.05l.83-.22.46-.12-.06-.46c-.21-1.53-1.62-3.25-3.94-4.8a37.57,37.57,0,0,0-5.22-2.82A13.36,13.36,0,0,1,11,21.19a3.36,3.36,0,0,1-.8-4.19c.41-.85.83-1.31,3.77-4.15,2.39-2.31,3.43-4.13,3.43-6a5.85,5.85,0,0,0-2.08-4.29c-.23-.21-.44-.43-.65-.65A2.5,2.5,0,0,1,15.27.69a10.6,10.6,0,0,1,2.91,2.78A4.16,4.16,0,0,1,19,6.16a4.91,4.91,0,0,1-.87,3c-.71,1.22-1.26,1.82-4.27,4.67a9.47,9.47,0,0,0-2.07,2.6,2.76,2.76,0,0,0-.33,1.54,2.76,2.76,0,0,0,.29,1.47c.57,1.21,2.23,2.55,4.65,3.73a32.41,32.41,0,0,1,5.82,3.24c2.16,1.6,3.2,3.16,3.2,4.8a1.94,1.94,0,0,0,.09.76,4.54,4.54,0,0,0,1.66-.4C27.29,31.42,27.29,31.37,27.14,30.86ZM6.1,7h0a3.77,3.77,0,0,1-1.46.45L4,7.51l.68-.83a25.09,25.09,0,0,0,3-4.82A12,12,0,0,1,8.28.76c.11-.12.77.32,1.53,1l.63.58-.57.84A10.34,10.34,0,0,1,6.1,7Zm5.71-1.78A9.77,9.77,0,0,1,9.24,7.18h0a5.25,5.25,0,0,1-1.17.28l-.58,0,.65-.78a21.29,21.29,0,0,0,2.1-3.12c.22-.41.42-.76.44-.79s.5.43.9,1.24L12,5ZM13.41,3a2.84,2.84,0,0,1-.45.64,11,11,0,0,1-.9-.91l-.84-.9.19-.45c.34-.79.39-.8,1-.31A9.4,9.4,0,0,1,13.8,2.33q-.18.34-.39.69Z" />
|
|
</svg>
|
|
<div class="grid">
|
|
<h4>RanchiMall CC</h4>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<section class="grid">
|
|
<h1 class="h2">FLO credentials</h1>
|
|
<p>Get your FLO credentials to use RanchiMall CC and all RanchiMall FLO apps. </p>
|
|
<div class="grid gap-1-5 card">
|
|
<div class="grid gap-0-5">
|
|
<h5>FLO ID</h5>
|
|
<sm-copy id="generated_flo_id"></sm-copy>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<h5>Private key</h5>
|
|
<sm-copy id="generated_private_key"></sm-copy>
|
|
</div>
|
|
</div>
|
|
<sm-button id="sign_up_button" variant="primary">Sign in with these credentials</sm-button>
|
|
<strong class="warning">
|
|
Keep your private key secure and don't share with anyone.
|
|
Once lost there is no way to recover private key.
|
|
</strong>
|
|
</section>
|
|
</article>
|
|
<article id="loading" class="page page-layout">
|
|
<sm-spinner></sm-spinner>
|
|
<h4>Loading RanchiMall CC</h4>
|
|
</article>
|
|
<article id="main_page" class="grid page hide-completely">
|
|
<header id="main_header">
|
|
<div id="article_name_wrapper" class="flex gap-0-5 align-center">
|
|
<button class="icon-only" title="Show all articles list" onclick="openPopup('article_list_popup')">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M4 8h4V4H4v4zm6 12h4v-4h-4v4zm-6 0h4v-4H4v4zm0-6h4v-4H4v4zm6 0h4v-4h-4v4zm6-10v4h4V4h-4zm-6 4h4V4h-4v4zm6 6h4v-4h-4v4zm0 6h4v-4h-4v4z" />
|
|
</svg>
|
|
</button>
|
|
<h4 id="current_article_title"></h4>
|
|
<button id="article_outline_button" class="icon-only" title="View article outline"
|
|
onclick="toggleOutlinePanel()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<g>
|
|
<rect fill="none" height="24" width="24" />
|
|
</g>
|
|
<g>
|
|
<path d="M9,18h12v-2H9V18z M3,6v2h18V6H3z M9,13h12v-2H9V13z" />
|
|
</g>
|
|
</svg>
|
|
</button>
|
|
<button id="filter_button" class="icon-only" title="Show filters" onclick="toggleFilterPanel()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" />
|
|
</svg>
|
|
</button>
|
|
<sm-menu align-options="right" title="Admin options" class="admin-option">
|
|
<menu-option onclick="openPopup('create_article_popup')">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
|
</svg>
|
|
Create new article
|
|
</menu-option>
|
|
<menu-option onclick="openPopup('edit_sections_popup')">
|
|
<svg class="icon margin-right-0-5" title="edit" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0z" fill="none"></path>
|
|
<path
|
|
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z">
|
|
</path>
|
|
</svg>
|
|
Edit title & sections
|
|
</menu-option>
|
|
<menu-option onclick="setDefaultArticle()">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg"
|
|
enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<g>
|
|
<path d="M0,0h24v24H0V0z" fill="none" />
|
|
</g>
|
|
<g>
|
|
<path
|
|
d="M14,2H6C4.9,2,4.01,2.9,4.01,4L4,20c0,1.1,0.89,2,1.99,2H18c1.1,0,2-0.9,2-2V8L14,2z M18,20H6V4h7v5h5V20z M8.82,13.05 L7.4,14.46L10.94,18l5.66-5.66l-1.41-1.41l-4.24,4.24L8.82,13.05z" />
|
|
</g>
|
|
</svg>
|
|
Make default article
|
|
</menu-option>
|
|
</sm-menu>
|
|
</div>
|
|
<div id="selected_content_options" class="flex space-between hide-completely">
|
|
<button onclick="clearSelection()" title="Clear selection">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
<div id="selected_entries_no"></div>
|
|
</button>
|
|
<div class="flex gap-0-5">
|
|
<button class="button icon-only" title="Select all" onclick="selectAll()">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M3 5h2V3c-1.1 0-2 .9-2 2zm0 8h2v-2H3v2zm4 8h2v-2H7v2zM3 9h2V7H3v2zm10-6h-2v2h2V3zm6 0v2h2c0-1.1-.9-2-2-2zM5 21v-2H3c0 1.1.9 2 2 2zm-2-4h2v-2H3v2zM9 3H7v2h2V3zm2 18h2v-2h-2v2zm8-8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2zm0-12h2V7h-2v2zm0 8h2v-2h-2v2zm-4 4h2v-2h-2v2zm0-16h2V3h-2v2zM7 17h10V7H7v10zm2-8h6v6H9V9z" />
|
|
</svg>
|
|
Select all
|
|
</button>
|
|
<button class="button" onclick="renderPreview()">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg"
|
|
enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<g>
|
|
<rect fill="none" height="24" width="24" />
|
|
<path
|
|
d="M19,3H5C3.89,3,3,3.9,3,5v14c0,1.1,0.89,2,2,2h14c1.1,0,2-0.9,2-2V5C21,3.9,20.11,3,19,3z M19,19H5V7h14V19z M12,10.5 c1.84,0,3.48,0.96,4.34,2.5c-0.86,1.54-2.5,2.5-4.34,2.5S8.52,14.54,7.66,13C8.52,11.46,10.16,10.5,12,10.5 M12,9 c-2.73,0-5.06,1.66-6,4c0.94,2.34,3.27,4,6,4s5.06-1.66,6-4C17.06,10.66,14.73,9,12,9L12,9z M12,14.5c-0.83,0-1.5-0.67-1.5-1.5 s0.67-1.5,1.5-1.5s1.5,0.67,1.5,1.5S12.83,14.5,12,14.5z" />
|
|
</g>
|
|
</svg>
|
|
Preview
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button onclick="openPopup('user_popup')" aria-label="User profile">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon margin-right-0-5" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M12 5.9c1.16 0 2.1.94 2.1 2.1s-.94 2.1-2.1 2.1S9.9 9.16 9.9 8s.94-2.1 2.1-2.1m0 9c2.97 0 6.1 1.46 6.1 2.1v1.1H5.9V17c0-.64 3.13-2.1 6.1-2.1M12 4C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 9c-2.67 0-8 1.34-8 4v3h16v-3c0-2.66-5.33-4-8-4z" />
|
|
</svg>
|
|
<div class="user-id"></div>
|
|
</button>
|
|
<theme-toggle></theme-toggle>
|
|
</header>
|
|
<section id="options_panel" class="flex hide-completely">
|
|
<div id="filter_panel" class="flex w-100 align-center gap-1 ">
|
|
<h5>Sort by</h5>
|
|
<strip-select id="sort_content_list" label="Sort by:">
|
|
<strip-option value="score" selected>Score</strip-option>
|
|
<strip-option value="time">Most recent</strip-option>
|
|
</strip-select>
|
|
</div>
|
|
<div id="article_outline_panel" class="flex align-center gap-1 hide-completely">
|
|
<h5>Sections</h5>
|
|
<ul id="article_outline" class="flex gap-1"></ul>
|
|
</div>
|
|
</section>
|
|
<div id="text_toolbar" class="hide-completely">
|
|
<div id="formatting_options">
|
|
<button id="strong_button" title="Bold (ctrl+b)" class="formatting-button" onclick="formatDoc('bold')">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z" />
|
|
</svg>
|
|
</button>
|
|
<button id="em_button" title="Italic (ctrl+i)" class="formatting-button" onclick="formatDoc('italic');">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4h-8z" />
|
|
</svg>
|
|
</button>
|
|
<button id="u_button" title="Underline (ctrl+u)" class="formatting-button"
|
|
onclick="formatDoc('underline');">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z" />
|
|
</svg>
|
|
</button>
|
|
<button id="sup_button" class="formatting-button" title="Superscript"
|
|
onclick="formatDoc('superscript')">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<g>
|
|
<rect fill="none" height="24" width="24" x="0" y="0" />
|
|
<path
|
|
d="M22,7h-2v1h3v1h-4V7c0-0.55,0.45-1,1-1h2V5h-3V4h3c0.55,0,1,0.45,1,1v1C23,6.55,22.55,7,22,7z M5.88,20h2.66l3.4-5.42h0.12 l3.4,5.42h2.66l-4.65-7.27L17.81,6h-2.68l-3.07,4.99h-0.12L8.85,6H6.19l4.32,6.73L5.88,20z" />
|
|
</g>
|
|
</svg>
|
|
</button>
|
|
<button id="sub_button" class="formatting-button" title="Subscript" onclick="formatDoc('subscript')">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<g>
|
|
<rect fill="none" height="24" width="24" />
|
|
<path
|
|
d="M22,18h-2v1h3v1h-4v-2c0-0.55,0.45-1,1-1h2v-1h-3v-1h3c0.55,0,1,0.45,1,1v1C23,17.55,22.55,18,22,18z M5.88,18h2.66 l3.4-5.42h0.12l3.4,5.42h2.66l-4.65-7.27L17.81,4h-2.68l-3.07,4.99h-0.12L8.85,4H6.19l4.32,6.73L5.88,18z" />
|
|
</g>
|
|
</svg>
|
|
</button>
|
|
<button id="a_button" class="formatting-button" title="Create link" onclick="toggleLinkPanel(this)">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M17 7h-4v2h4c1.65 0 3 1.35 3 3s-1.35 3-3 3h-4v2h4c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-6 8H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-2zm-3-4h8v2H8z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div id="link_panel" class="hide-completely">
|
|
<sm-form style="--gap: 0.5rem">
|
|
<button class="icon-only justify-self-start" onclick="toggleLinkPanel()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
</svg>
|
|
</button>
|
|
<sm-input id="create_link_href" placeholder="Link address" required hiderequired></sm-input>
|
|
<sm-button variant="primary" onclick="createLink()">Add link</sm-button>
|
|
</sm-form>
|
|
</div>
|
|
</div>
|
|
<div id="article_wrapper"></div>
|
|
<aside id="version_history_panel" class="flex direction-column hide-completely">
|
|
<div class="flex align-center space-between">
|
|
<div class="flex align-center">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.25 2.52.77-1.28-3.52-2.09V8z" />
|
|
</svg>
|
|
<h4>Version history</h4>
|
|
</div>
|
|
<button onclick="hideVersionHistory()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<ul id="version_timeline" class="flex direction-column gap-1-5"></ul>
|
|
</aside>
|
|
</article>
|
|
<article id="preview_page" class="grid page hide-completely">
|
|
<header class="grid gap-1 full-bleed">
|
|
<div class="flex align-center space-between">
|
|
<div class="flex gap-0-5">
|
|
<button onclick="goHome()">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
|
|
viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
</svg>
|
|
Preview
|
|
</button>
|
|
</div>
|
|
<div id="preview_options" class="flex align-center gap-0-5"></div>
|
|
</div>
|
|
<h1 id="preview__title" class="article__title"></h1>
|
|
</header>
|
|
<main id="preview__body"></main>
|
|
</article>
|
|
<sm-popup id="article_list_popup">
|
|
<header slot="header" class="popup__header">
|
|
<div class="flex align-center">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>All articles</h3>
|
|
</div>
|
|
<sm-input id="article_list_search" placeholder="Search articles" type="search" autofocus>
|
|
<svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0z" fill="none" />
|
|
<path
|
|
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
|
</svg>
|
|
</sm-input>
|
|
<sm-select id="sort_article_list" label="Sort by:">
|
|
<sm-option value="time">Most recent</sm-option>
|
|
<sm-option value="az">A-Z</sm-option>
|
|
</sm-select>
|
|
</header>
|
|
<div id="article_list" class="observe-empty-state grid"></div>
|
|
<h4 class="empty-state">
|
|
No related article
|
|
</h4>
|
|
</sm-popup>
|
|
<sm-popup id="create_article_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Create article</h3>
|
|
</header>
|
|
<sm-form>
|
|
<sm-input id="get_article_title" placeholder="Article title" required autofocus></sm-input>
|
|
<sm-checkbox id="set_default_checkbox" checked>
|
|
<div class="grid margin-left-0-5 gap-0-5">
|
|
Set as default
|
|
<p style="font-size: 0.8rem;">
|
|
This article will be opened by default for everyone when CC is first loaded.
|
|
</p>
|
|
</div>
|
|
</sm-checkbox>
|
|
<div class="grid gap-1">
|
|
<div class="grid gap-0-5">
|
|
<h4>Define plot (optional)</h4>
|
|
<p>Create and name sections by writing section title encapsulated in '()' and each separated by a
|
|
'->'.
|
|
</p>
|
|
</div>
|
|
<sm-textarea id="get_plot" rows="8"
|
|
placeholder="(Name, place, time, context) -> (Name, place, time, context) ...">
|
|
</sm-textarea>
|
|
</div>
|
|
<!-- <div class="grid gap-1">
|
|
<div class="grid gap-0-5">
|
|
<sm-switch>
|
|
<h4 slot="left">
|
|
Make private
|
|
</h4>
|
|
</sm-switch>
|
|
<p>Define the FLO IDs separated by a comma (,), which are allowed to contribute.</p>
|
|
</div>
|
|
<sm-textarea id="get_plot" rows="4"></sm-textarea>
|
|
</div> -->
|
|
<sm-button variant="primary" onclick="cc.createNewArticle()">Create</sm-button>
|
|
</sm-form>
|
|
</sm-popup>
|
|
<sm-popup id="edit_sections_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Edit title & sections</h3>
|
|
</header>
|
|
<section class="grid gap-1-5">
|
|
<div class="grid gap-0-5">
|
|
<h5 class="label">Title</h5>
|
|
<sm-input id="edit_article_title"></sm-input>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<h5 class="label">Sections</h5>
|
|
<div id="section_list_container" class="observe-empty-state grid"></div>
|
|
<p class="empty-state">
|
|
There are no sections so far, you can add section with button below.
|
|
</p>
|
|
</div>
|
|
<div class="flex space-between align-center">
|
|
<button id="insert_section_button" class="button" onclick="insertEmptySection()">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg"
|
|
enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<g>
|
|
<rect fill="none" height="24" width="24" />
|
|
</g>
|
|
<g>
|
|
<g />
|
|
<g>
|
|
<path
|
|
d="M17,19.22H5V7h7V5H5C3.9,5,3,5.9,3,7v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-7h-2V19.22z" />
|
|
<path d="M19,2h-2v3h-3c0.01,0.01,0,2,0,2h3v2.99c0.01,0.01,2,0,2,0V7h3V5h-3V2z" />
|
|
<rect height="2" width="8" x="7" y="9" />
|
|
<polygon points="7,12 7,14 15,14 15,12 12,12" />
|
|
<rect height="2" width="8" x="7" y="15" />
|
|
</g>
|
|
</g>
|
|
</svg>
|
|
Insert section
|
|
</button>
|
|
<button class="button button--primary" onclick="saveSectionEdit()">Save</button>
|
|
</div>
|
|
</section>
|
|
</sm-popup>
|
|
<sm-popup id="contributors_popup">
|
|
<header slot="header" class="popup__header">
|
|
<div class="flex align-center">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Contributors</h3>
|
|
</div>
|
|
</header>
|
|
<div id="contributor_list" class="grid gap-1-5"></div>
|
|
</sm-popup>
|
|
<sm-popup id="share_popup">
|
|
<header slot="header" class="popup__header">
|
|
<div class="flex align-center">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Share article preview</h3>
|
|
</div>
|
|
</header>
|
|
<span class="label">Copy link</span>
|
|
<sm-copy id="shared_url" clip-text></sm-copy>
|
|
</sm-popup>
|
|
<sm-popup id="user_popup">
|
|
<header slot="header" class="popup__header">
|
|
<div class="flex align-center">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
<section class="grid gap-1-5">
|
|
<div class="grid gap-0-5">
|
|
<h5>My FLO ID</h5>
|
|
<sm-copy id="user_flo_id"></sm-copy>
|
|
</div>
|
|
<sm-button class="danger" onclick="signOut()">Sign out</sm-button>
|
|
</section>
|
|
</sm-popup>
|
|
<sm-popup id="scoring_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Update score</h3>
|
|
</header>
|
|
<sm-form>
|
|
<sm-input id="update_score_field" class="outlined" placeholder="Score" type="number" step="0.1" min="0"
|
|
max="100" error-text="Value must be between 1-100" autofocus animate required hiderequired></sm-input>
|
|
<div id="inc_section" class="flex">
|
|
<button class="button" onclick="incScore(0.5)">+ 0.5</button>
|
|
<button class="button" onclick="incScore(1)">+ 1</button>
|
|
<button class="button" onclick="incScore(5)">+ 5</button>
|
|
<sm-button id="get_new_score" variant="primary" class="justify-right" onclick="updateScore()" disabled>
|
|
Update</sm-button>
|
|
</div>
|
|
</sm-form>
|
|
</sm-popup>
|
|
<template id="contributor_template">
|
|
<div class="contributor grid">
|
|
<svg class="icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
d="M14.6243 7.99154C13.9395 8.14279 13.4165 8.69672 13.3047 9.38904C13.2248 9.88365 13.3664 10.3773 13.6735 10.7498L9.29125 15.0941C8.93718 15.4451 8.48852 15.6853 8.00011 15.7854L6.12418 16.1699L9.19811 13.1381C9.53438 12.8064 9.53812 12.265 9.20646 11.9287C8.8748 11.5924 8.33333 11.5887 7.99706 11.9203L4.97835 14.8977L5.47976 12.451C5.50451 12.3303 5.55656 12.2168 5.63191 12.1192L6.49083 11.0073C9.58386 7.00317 14.32 4.72705 19.2553 4.71054L16.3569 7.60884L14.6243 7.99154ZM2.79008 17.0559L3.8042 12.1076C3.88132 11.7313 4.0435 11.3776 4.27833 11.0736L5.13724 9.96172C8.89964 5.09105 14.861 2.53305 20.8954 3.07051C21.5971 3.133 22.2997 3.23735 23 3.38478L17.2133 9.1713L14.9932 9.66167L15.4238 9.91837C15.9039 10.2046 15.9849 10.8668 15.588 11.2603L10.4954 16.3088C9.90529 16.8938 9.15752 17.2941 8.34351 17.461L3.88965 18.3738L2.45572 19.788C2.11945 20.1197 1.57798 20.116 1.24632 19.7797C0.91466 19.4434 0.918397 18.9019 1.25467 18.5703L2.79008 17.0559Z" />
|
|
</svg>
|
|
<div class="contributor__id breakable"></div>
|
|
<time class="contributor__time"></time>
|
|
</div>
|
|
</template>
|
|
<template id="section_template">
|
|
<div class="heading flex align-center">
|
|
<h4 class="section-title"></h4>
|
|
</div>
|
|
<section class="article-section">
|
|
<div class="content-card content-card--empty">
|
|
<div class="content__area" data-type="origin" placeholder="Write something new or edit existing content"
|
|
contenteditable="true"></div>
|
|
<button class="submit-entry hide-completely">Submit</button>
|
|
</div>
|
|
<!-- <div class="content-card-container"></div> -->
|
|
</section>
|
|
</template>
|
|
<template id="content_card_template">
|
|
<div class="content-card">
|
|
<div class="content__header flex align-center space-between">
|
|
<Button class="content__contributors flex align-center" title="Contributors">
|
|
<svg class="icon" width="24" height="24" style="margin-right: 0.3rem;" viewBox="0 0 24 24"
|
|
fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
d="M14.6243 7.99154C13.9395 8.14279 13.4165 8.69672 13.3047 9.38904C13.2248 9.88365 13.3664 10.3773 13.6735 10.7498L9.29125 15.0941C8.93718 15.4451 8.48852 15.6853 8.00011 15.7854L6.12418 16.1699L9.19811 13.1381C9.53438 12.8064 9.53812 12.265 9.20646 11.9287C8.8748 11.5924 8.33333 11.5887 7.99706 11.9203L4.97835 14.8977L5.47976 12.451C5.50451 12.3303 5.55656 12.2168 5.63191 12.1192L6.49083 11.0073C9.58386 7.00317 14.32 4.72705 19.2553 4.71054L16.3569 7.60884L14.6243 7.99154ZM2.79008 17.0559L3.8042 12.1076C3.88132 11.7313 4.0435 11.3776 4.27833 11.0736L5.13724 9.96172C8.89964 5.09105 14.861 2.53305 20.8954 3.07051C21.5971 3.133 22.2997 3.23735 23 3.38478L17.2133 9.1713L14.9932 9.66167L15.4238 9.91837C15.9039 10.2046 15.9849 10.8668 15.588 11.2603L10.4954 16.3088C9.90529 16.8938 9.15752 17.2941 8.34351 17.461L3.88965 18.3738L2.45572 19.788C2.11945 20.1197 1.57798 20.116 1.24632 19.7797C0.91466 19.4434 0.918397 18.9019 1.25467 18.5703L2.79008 17.0559Z" />
|
|
</svg>
|
|
<div class="content__author flex align-center"></div>
|
|
</Button>
|
|
<sm-checkbox class="content__checkbox" aria-label="Select content"
|
|
title="Select this snippet to download as HTML"></sm-checkbox>
|
|
</div>
|
|
<div class="content__area"></div>
|
|
<div class="flex align-center space-between">
|
|
<div class="content__options grid align-center">
|
|
<button class="version-history-button" title="See version history">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.25 2.52.77-1.28-3.52-2.09V8z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<button class="submit-entry hide-completely">Submit</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template id="history_entry_template">
|
|
<li class="history-entry grid gap-1">
|
|
<div class="flex align-center space-between">
|
|
<time class="entry__time"></time>
|
|
</div>
|
|
<div class="grid">
|
|
<div class="label flex align-center">
|
|
<svg class="icon" style="margin-right: 0.2rem;" width="24" height="24" viewBox="0 0 24 24"
|
|
fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
d="M14.6243 7.99154C13.9395 8.14279 13.4165 8.69672 13.3047 9.38904C13.2248 9.88365 13.3664 10.3773 13.6735 10.7498L9.29125 15.0941C8.93718 15.4451 8.48852 15.6853 8.00011 15.7854L6.12418 16.1699L9.19811 13.1381C9.53438 12.8064 9.53812 12.265 9.20646 11.9287C8.8748 11.5924 8.33333 11.5887 7.99706 11.9203L4.97835 14.8977L5.47976 12.451C5.50451 12.3303 5.55656 12.2168 5.63191 12.1192L6.49083 11.0073C9.58386 7.00317 14.32 4.72705 19.2553 4.71054L16.3569 7.60884L14.6243 7.99154ZM2.79008 17.0559L3.8042 12.1076C3.88132 11.7313 4.0435 11.3776 4.27833 11.0736L5.13724 9.96172C8.89964 5.09105 14.861 2.53305 20.8954 3.07051C21.5971 3.133 22.2997 3.23735 23 3.38478L17.2133 9.1713L14.9932 9.66167L15.4238 9.91837C15.9039 10.2046 15.9849 10.8668 15.588 11.2603L10.4954 16.3088C9.90529 16.8938 9.15752 17.2941 8.34351 17.461L3.88965 18.3738L2.45572 19.788C2.11945 20.1197 1.57798 20.116 1.24632 19.7797C0.91466 19.4434 0.918397 18.9019 1.25467 18.5703L2.79008 17.0559Z" />
|
|
</svg>
|
|
Author
|
|
</div>
|
|
<span class="entry__author breakable"></span>
|
|
</div>
|
|
<div class="entry__changes"></div>
|
|
</li>
|
|
</template>
|
|
<script id="ui_utils">
|
|
const { html, render: renderElem } = uhtml;
|
|
const domRefs = {}
|
|
//Checks for internet connection status
|
|
if (!navigator.onLine)
|
|
notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error', '', true)
|
|
window.addEventListener('offline', () => {
|
|
notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error', true, true)
|
|
})
|
|
window.addEventListener('online', () => {
|
|
getRef('notification_drawer').clearAll()
|
|
notify('We are back online.', 'success')
|
|
})
|
|
|
|
// Use instead of document.getElementById
|
|
function getRef(elementId) {
|
|
if (!domRefs.hasOwnProperty(elementId)) {
|
|
domRefs[elementId] = {
|
|
count: 1,
|
|
ref: null,
|
|
};
|
|
return document.getElementById(elementId);
|
|
} else {
|
|
if (domRefs[elementId].count < 3) {
|
|
domRefs[elementId].count = domRefs[elementId].count + 1;
|
|
return document.getElementById(elementId);
|
|
} else {
|
|
if (!domRefs[elementId].ref)
|
|
domRefs[elementId].ref = document.getElementById(elementId);
|
|
return domRefs[elementId].ref;
|
|
}
|
|
}
|
|
}
|
|
|
|
// returns dom with specified element
|
|
function createElement(tagName, options = {}) {
|
|
const { className, textContent, innerHTML, attributes = {} } = options
|
|
const elem = document.createElement(tagName)
|
|
for (let attribute in attributes) {
|
|
elem.setAttribute(attribute, attributes[attribute])
|
|
}
|
|
if (className)
|
|
elem.className = className
|
|
if (textContent)
|
|
elem.textContent = textContent
|
|
if (innerHTML)
|
|
elem.innerHTML = innerHTML
|
|
return elem
|
|
}
|
|
|
|
// Use when a function needs to be executed after user finishes changes
|
|
const debounce = (callback, wait) => {
|
|
let timeoutId = null;
|
|
return (...args) => {
|
|
window.clearTimeout(timeoutId);
|
|
timeoutId = window.setTimeout(() => {
|
|
callback.apply(null, args);
|
|
}, wait);
|
|
};
|
|
}
|
|
// adds a class to all elements in an array
|
|
function addClass(elements, className) {
|
|
elements.forEach((element) => {
|
|
document.querySelector(element).classList.add(className);
|
|
});
|
|
}
|
|
// removes a class from all elements in an array
|
|
function removeClass(elements, className) {
|
|
elements.forEach((element) => {
|
|
document.querySelector(element).classList.remove(className);
|
|
});
|
|
}
|
|
// return querySelectorAll elements as an array
|
|
function getAllElements(selector) {
|
|
return Array.from(document.querySelectorAll(selector));
|
|
}
|
|
|
|
let zIndex = 50
|
|
// function required for popups or modals to appear
|
|
function openPopup(popupId, pinned) {
|
|
zIndex++
|
|
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
|
|
getRef(popupId).show({ pinned })
|
|
return getRef(popupId);
|
|
}
|
|
|
|
// hides the popup or modal
|
|
function closePopup() {
|
|
if (popupStack.peek() === undefined)
|
|
return;
|
|
popupStack.peek().popup.hide()
|
|
}
|
|
|
|
document.addEventListener('popupopened', e => {
|
|
switch (e.target.id) {
|
|
case 'edit_sections_popup':
|
|
renderSectionList()
|
|
break;
|
|
case 'article_list_popup':
|
|
renderArticleList()
|
|
break;
|
|
}
|
|
})
|
|
document.addEventListener('popupclosed', e => {
|
|
zIndex--
|
|
switch (e.target.id) {
|
|
case 'edit_sections_popup':
|
|
getRef('section_list_container').innerHTML = ''
|
|
break;
|
|
case 'article_list_popup':
|
|
renderElem(getRef('article_list'), html``)
|
|
break;
|
|
case 'contributors_popup':
|
|
getRef('contributor_list').innerHTML = ''
|
|
break;
|
|
}
|
|
})
|
|
|
|
// displays a popup for asking permission. Use this instead of JS confirm
|
|
const getConfirmation = (title, options = {}) => {
|
|
return new Promise(resolve => {
|
|
const { message = '', cancelText = 'Cancel', confirmText = 'OK' } = options
|
|
openPopup('confirmation_popup', true)
|
|
getRef('confirm_title').innerText = title;
|
|
getRef('confirm_message').innerText = message;
|
|
let cancelButton = getRef('confirmation_popup').children[2].children[0],
|
|
submitButton = getRef('confirmation_popup').children[2].children[1]
|
|
submitButton.textContent = confirmText
|
|
cancelButton.textContent = cancelText
|
|
submitButton.onclick = () => {
|
|
closePopup()
|
|
resolve(true);
|
|
}
|
|
cancelButton.onclick = () => {
|
|
closePopup()
|
|
resolve(false);
|
|
}
|
|
})
|
|
}
|
|
|
|
//Function for displaying toast notifications. pass in error for mode param if you want to show an error.
|
|
function notify(message, mode, options = {}) {
|
|
let icon
|
|
switch (mode) {
|
|
case 'success':
|
|
icon = `<svg class="icon icon--success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></svg>`
|
|
break;
|
|
case 'error':
|
|
icon = `<svg class="icon icon--error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>`
|
|
break;
|
|
}
|
|
getRef("notification_drawer").push(message, { icon, ...options });
|
|
if (mode === 'error') {
|
|
console.error(message)
|
|
}
|
|
}
|
|
|
|
function getFormattedTime(timestamp, format) {
|
|
try {
|
|
if (String(timestamp).length < 13)
|
|
timestamp *= 1000
|
|
let [day, month, date, year] = new Date(timestamp).toString().split(' '),
|
|
minutes = new Date(timestamp).getMinutes(),
|
|
hours = new Date(timestamp).getHours(),
|
|
currentTime = new Date().toString().split(' ')
|
|
|
|
minutes = minutes < 10 ? `0${minutes}` : minutes
|
|
let finalHours = ``;
|
|
if (hours > 12)
|
|
finalHours = `${hours - 12}:${minutes}`
|
|
else if (hours === 0)
|
|
finalHours = `12:${minutes}`
|
|
else
|
|
finalHours = `${hours}:${minutes}`
|
|
|
|
finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM`
|
|
switch (format) {
|
|
case 'date-only':
|
|
return `${month} ${date}, ${year}`;
|
|
break;
|
|
case 'time-only':
|
|
return finalHours;
|
|
case 'relative':
|
|
// check if timestamp is older than a day
|
|
if (Date.now() - new Date(timestamp) < 60 * 60 * 24 * 1000)
|
|
return `${finalHours}`;
|
|
else
|
|
return relativeTime.from(timestamp)
|
|
default:
|
|
return `${month} ${date}, ${year} at ${finalHours}`;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
return timestamp;
|
|
}
|
|
}
|
|
|
|
window.addEventListener('hashchange', e => showPage(window.location.hash))
|
|
window.addEventListener("load", () => {
|
|
document.body.classList.remove('hide-completely')
|
|
document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex)
|
|
document.addEventListener('keyup', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closePopup()
|
|
}
|
|
})
|
|
document.addEventListener('copy', () => {
|
|
notify('copied', 'success')
|
|
})
|
|
document.addEventListener("pointerdown", (e) => {
|
|
if (e.target.closest("button, sm-button:not([disabled]), .interact")) {
|
|
createRipple(e, e.target.closest("button, sm-button, .interact"));
|
|
}
|
|
});
|
|
});
|
|
function createRipple(event, target) {
|
|
const circle = document.createElement("span");
|
|
const diameter = Math.max(target.clientWidth, target.clientHeight);
|
|
const radius = diameter / 2;
|
|
const targetDimensions = target.getBoundingClientRect();
|
|
circle.style.width = circle.style.height = `${diameter}px`;
|
|
circle.style.left = `${event.clientX - (targetDimensions.left + radius)}px`;
|
|
circle.style.top = `${event.clientY - (targetDimensions.top + radius)}px`;
|
|
circle.classList.add("ripple");
|
|
const rippleAnimation = circle.animate(
|
|
[
|
|
{
|
|
transform: "scale(4)",
|
|
opacity: 0,
|
|
},
|
|
],
|
|
{
|
|
duration: 600,
|
|
fill: "forwards",
|
|
easing: "ease-out",
|
|
}
|
|
);
|
|
target.append(circle);
|
|
rippleAnimation.onfinish = () => {
|
|
circle.remove();
|
|
};
|
|
}
|
|
|
|
const pagesData = {
|
|
openedPages: [],
|
|
params: {}
|
|
}
|
|
|
|
async function showPage(targetPage, options = {}) {
|
|
const { firstLoad, hashChange, isPreview } = options
|
|
let pageId
|
|
let params = {}
|
|
let searchParams
|
|
if (targetPage === '') {
|
|
if (typeof myFloID === "undefined") {
|
|
pageId = 'landing'
|
|
} else {
|
|
pageId = 'main_page'
|
|
}
|
|
} else {
|
|
if (targetPage.includes('/')) {
|
|
if (targetPage.includes('?')) {
|
|
const splitAddress = targetPage.split('?')
|
|
searchParams = splitAddress.pop()
|
|
const pages = splitAddress.pop().split('/')
|
|
pageId = pages[1]
|
|
subPageId = pages[2]
|
|
} else {
|
|
const pages = targetPage.split('/')
|
|
pageId = pages[1]
|
|
subPageId = pages[2]
|
|
}
|
|
} else {
|
|
pageId = targetPage
|
|
}
|
|
}
|
|
if (isPreview) {
|
|
floGlobals.preview = location.hash;
|
|
}
|
|
if (typeof myFloID === "undefined" && !(['sign_up', 'sign_in', 'loading', 'landing'].includes(pageId))) return
|
|
if (searchParams) {
|
|
const urlSearchParams = new URLSearchParams('?' + searchParams);
|
|
params = Object.fromEntries(urlSearchParams.entries());
|
|
}
|
|
if (firstLoad || params.articleID !== pagesData.params.articleID) {
|
|
if (!params.articleID)
|
|
params['articleID'] = floGlobals.appObjects.cc.defaultArticle
|
|
await Promise.all([
|
|
floCloudAPI.requestObjectData(params.articleID),
|
|
floCloudAPI.requestGeneralData(`${params.articleID}_gd`)
|
|
])
|
|
}
|
|
switch (pageId) {
|
|
case 'landing':
|
|
targetPage = 'landing'
|
|
break;
|
|
case 'sign_in':
|
|
setTimeout(() => {
|
|
getRef('private_key_field').focusIn()
|
|
}, 0);
|
|
targetPage = 'sign_in'
|
|
break;
|
|
case 'sign_up':
|
|
const { floID, privKey } = floCrypto.generateNewID()
|
|
getRef('generated_flo_id').value = floID
|
|
getRef('generated_private_key').value = privKey
|
|
targetPage = 'sign_up'
|
|
break;
|
|
case 'home':
|
|
case 'main_page':
|
|
if (!floGlobals.currentArticle.id || params.articleID !== pagesData.params.articleID) {
|
|
closePopup()
|
|
render.article(params.articleID)
|
|
}
|
|
hideVersionHistory()
|
|
window.history.replaceState('', '', `#/home?articleID=${params.articleID}`)
|
|
renderElem(getRef('preview__body'), html``)
|
|
targetPage = 'main_page'
|
|
break;
|
|
case 'preview':
|
|
case 'preview_page':
|
|
if (params.uid) {
|
|
getRef('preview__title').textContent = floGlobals.appObjects.cc.articleList[params.articleID].title
|
|
getRef('preview__body').innerHTML = floGlobals.appObjects[params.articleID].preview.content
|
|
}
|
|
targetPage = 'preview_page'
|
|
break
|
|
}
|
|
document.querySelectorAll('.page').forEach(page => page.classList.add('hide-completely'))
|
|
getRef(targetPage).classList.remove('hide-completely')
|
|
if (pagesData.lastPage !== pageId) {
|
|
pagesData.lastPage = pageId
|
|
if (!pagesData.openedPages.includes(pageId)) {
|
|
pagesData.openedPages.push(pageId)
|
|
}
|
|
}
|
|
if (params)
|
|
pagesData.params = params
|
|
}
|
|
// class based lazy loading
|
|
class LazyLoader {
|
|
constructor(container, elementsToRender, renderFn, options = {}) {
|
|
const { batchSize = 10, freshRender, bottomFirst = false, domUpdated } = options
|
|
|
|
this.elementsToRender = elementsToRender
|
|
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
|
|
this.renderFn = renderFn
|
|
this.intersectionObserver
|
|
|
|
this.batchSize = batchSize
|
|
this.freshRender = freshRender
|
|
this.domUpdated = domUpdated
|
|
this.bottomFirst = bottomFirst
|
|
|
|
this.shouldLazyLoad = false
|
|
this.lastScrollTop = 0
|
|
this.lastScrollHeight = 0
|
|
|
|
this.lazyContainer = document.querySelector(container)
|
|
|
|
this.update = this.update.bind(this)
|
|
this.render = this.render.bind(this)
|
|
this.init = this.init.bind(this)
|
|
this.clear = this.clear.bind(this)
|
|
}
|
|
get elements() {
|
|
return this.arrayOfElements
|
|
}
|
|
init() {
|
|
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
observer.disconnect()
|
|
this.render({ lazyLoad: true })
|
|
}
|
|
})
|
|
}, {
|
|
root: this.lazyContainer
|
|
})
|
|
this.mutationObserver = new MutationObserver(mutationList => {
|
|
mutationList.forEach(mutation => {
|
|
if (mutation.type === 'childList') {
|
|
if (mutation.addedNodes.length) {
|
|
if (this.bottomFirst) {
|
|
if (this.lazyContainer.firstElementChild)
|
|
this.intersectionObserver.observe(this.lazyContainer.firstElementChild)
|
|
} else {
|
|
if (this.lazyContainer.lastElementChild)
|
|
this.intersectionObserver.observe(this.lazyContainer.lastElementChild)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
this.mutationObserver.observe(this.lazyContainer, {
|
|
childList: true,
|
|
})
|
|
this.render()
|
|
}
|
|
update(elementsToRender) {
|
|
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
|
|
}
|
|
render(options = {}) {
|
|
let { lazyLoad = false } = options
|
|
this.shouldLazyLoad = lazyLoad
|
|
const frag = document.createDocumentFragment();
|
|
if (lazyLoad) {
|
|
if (this.bottomFirst) {
|
|
this.updateEndIndex = this.updateStartIndex
|
|
this.updateStartIndex = this.updateEndIndex - this.batchSize
|
|
} else {
|
|
this.updateStartIndex = this.updateEndIndex
|
|
this.updateEndIndex = this.updateEndIndex + this.batchSize
|
|
}
|
|
} else {
|
|
this.intersectionObserver.disconnect()
|
|
if (this.bottomFirst) {
|
|
this.updateEndIndex = this.arrayOfElements.length
|
|
this.updateStartIndex = this.updateEndIndex - this.batchSize - 1
|
|
} else {
|
|
this.updateStartIndex = 0
|
|
this.updateEndIndex = this.batchSize
|
|
}
|
|
this.lazyContainer.innerHTML = ``;
|
|
}
|
|
this.lastScrollHeight = this.lazyContainer.scrollHeight
|
|
this.lastScrollTop = this.lazyContainer.scrollTop
|
|
this.arrayOfElements.slice(this.updateStartIndex, this.updateEndIndex).forEach((element, index) => {
|
|
frag.append(this.renderFn(element))
|
|
})
|
|
if (this.bottomFirst) {
|
|
this.lazyContainer.prepend(frag)
|
|
// scroll anchoring for reverse scrolling
|
|
this.lastScrollTop += this.lazyContainer.scrollHeight - this.lastScrollHeight
|
|
this.lazyContainer.scrollTo({ top: this.lastScrollTop })
|
|
this.lastScrollHeight = this.lazyContainer.scrollHeight
|
|
} else {
|
|
this.lazyContainer.append(frag)
|
|
}
|
|
if (!lazyLoad && this.bottomFirst) {
|
|
this.lazyContainer.scrollTop = this.lazyContainer.scrollHeight
|
|
}
|
|
// Callback to be called if elements are updated or rendered for first time
|
|
if (!lazyLoad && this.freshRender)
|
|
this.freshRender()
|
|
}
|
|
clear() {
|
|
this.intersectionObserver.disconnect()
|
|
this.mutationObserver.disconnect()
|
|
this.lazyContainer.innerHTML = ``;
|
|
}
|
|
reset() {
|
|
this.arrayOfElements = (typeof this.elementsToRender === 'function') ? this.elementsToRender() : this.elementsToRender || []
|
|
this.render()
|
|
}
|
|
}
|
|
function animateTo(element, keyframes, options) {
|
|
const anime = element.animate(keyframes, { ...options, fill: 'both' })
|
|
anime.finished.then(() => {
|
|
anime.commitStyles()
|
|
anime.cancel()
|
|
})
|
|
return anime
|
|
}
|
|
</script>
|
|
<script>
|
|
// JS Diff
|
|
!function (e, n) { "object" == typeof exports && "undefined" != typeof module ? n(exports) : "function" == typeof define && define.amd ? define(["exports"], n) : n((e = e || self).Diff = {}) }(this, function (e) { "use strict"; function t() { } t.prototype = { diff: function (s, u, e) { var e = 2 < arguments.length && void 0 !== e ? e : {}, n = e.callback; "function" == typeof e && (n = e, e = {}), this.options = e; var a = this; function f(e) { return n ? (setTimeout(function () { n(void 0, e) }, 0), !0) : e } s = this.castInput(s), u = this.castInput(u), s = this.removeEmpty(this.tokenize(s)); var d = (u = this.removeEmpty(this.tokenize(u))).length, c = s.length, h = 1, t = d + c, p = [{ newPos: -1, components: [] }], e = this.extractCommon(p[0], u, s, 0); if (p[0].newPos + 1 >= d && c <= e + 1) return f([{ value: this.join(u), count: u.length }]); function r() { for (var e = -1 * h; e <= h; e += 2) { var n = void 0, t = p[e - 1], r = p[e + 1], i = (r ? r.newPos : 0) - e; t && (p[e - 1] = void 0); var o = t && t.newPos + 1 < d, l = r && 0 <= i && i < c; if (o || l) { if (!o || l && t.newPos < r.newPos ? (n = { newPos: (r = r).newPos, components: r.components.slice(0) }, a.pushComponent(n.components, void 0, !0)) : ((n = t).newPos++, a.pushComponent(n.components, !0, void 0)), i = a.extractCommon(n, u, s, e), n.newPos + 1 >= d && c <= i + 1) return f(function (e, n, t, r, i) { for (var o = 0, l = n.length, s = 0, u = 0; o < l; o++) { var a, f = n[o]; f.removed ? (f.value = e.join(r.slice(u, u + f.count)), u += f.count, o && n[o - 1].added && (a = n[o - 1], n[o - 1] = n[o], n[o] = a)) : (!f.added && i ? (a = (a = t.slice(s, s + f.count)).map(function (e, n) { n = r[u + n]; return n.length > e.length ? n : e }), f.value = e.join(a)) : f.value = e.join(t.slice(s, s + f.count)), s += f.count, f.added || (u += f.count)) } var d = n[l - 1]; 1 < l && "string" == typeof d.value && (d.added || d.removed) && e.equals("", d.value) && (n[l - 2].value += d.value, n.pop()); return n }(a, n.components, u, s, a.useLongestToken)); p[e] = n } else p[e] = void 0 } h++ } if (n) !function e() { setTimeout(function () { return t < h ? n() : void (r() || e()) }, 0) }(); else for (; h <= t;) { var i = r(); if (i) return i } }, pushComponent: function (e, n, t) { var r = e[e.length - 1]; r && r.added === n && r.removed === t ? e[e.length - 1] = { count: r.count + 1, added: n, removed: t } : e.push({ count: 1, added: n, removed: t }) }, extractCommon: function (e, n, t, r) { for (var i = n.length, o = t.length, l = e.newPos, s = l - r, u = 0; l + 1 < i && s + 1 < o && this.equals(n[l + 1], t[s + 1]);)l++, s++, u++; return u && e.components.push({ count: u }), e.newPos = l, s }, equals: function (e, n) { return this.options.comparator ? this.options.comparator(e, n) : e === n || this.options.ignoreCase && e.toLowerCase() === n.toLowerCase() }, removeEmpty: function (e) { for (var n = [], t = 0; t < e.length; t++)e[t] && n.push(e[t]); return n }, castInput: function (e) { return e }, tokenize: function (e) { return e.split("") }, join: function (e) { return e.join("") } }; var r = new t; function i(e, n) { if ("function" == typeof e) n.callback = e; else if (e) for (var t in e) e.hasOwnProperty(t) && (n[t] = e[t]); return n } var o = /^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/, l = /\S/, s = new t; s.equals = function (e, n) { return this.options.ignoreCase && (e = e.toLowerCase(), n = n.toLowerCase()), e === n || this.options.ignoreWhitespace && !l.test(e) && !l.test(n) }, s.tokenize = function (e) { for (var n = e.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/), t = 0; t < n.length - 1; t++)!n[t + 1] && n[t + 2] && o.test(n[t]) && o.test(n[t + 2]) && (n[t] += n[t + 2], n.splice(t + 1, 2), t--); return n }; var u = new t; function m(e, n, t) { return u.diff(e, n, t) } u.tokenize = function (e) { var n = [], t = e.split(/(\n|\r\n)/); t[t.length - 1] || t.pop(); for (var r = 0; r < t.length; r++) { var i = t[r]; r % 2 && !this.options.newlineIsToken ? n[n.length - 1] += i : (this.options.ignoreWhitespace && (i = i.trim()), n.push(i)) } return n }; var a = new t; a.tokenize = function (e) { return e.split(/(\S.+?[.!?])(?=\s+|$)/) }; var f = new t; function d(e) { return (d = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (e) { return typeof e } : function (e) { return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e })(e) } function w(e) { return function (e) { if (Array.isArray(e)) return c(e) }(e) || function (e) { if ("undefined" != typeof Symbol && Symbol.iterator in Object(e)) return Array.from(e) }(e) || function (e, n) { if (!e) return; if ("string" == typeof e) return c(e, n); var t = Object.prototype.toString.call(e).slice(8, -1); "Object" === t && e.constructor && (t = e.constructor.name); if ("Map" === t || "Set" === t) return Array.from(e); if ("Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t)) return c(e, n) }(e) || function () { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.") }() } function c(e, n) { (null == n || n > e.length) && (n = e.length); for (var t = 0, r = new Array(n); t < n; t++)r[t] = e[t]; return r } f.tokenize = function (e) { return e.split(/([{}:;,]|\s+)/) }; var h = Object.prototype.toString, p = new t; function v(e, n, t, r, i) { var o, l; for (n = n || [], t = t || [], r && (e = r(i, e)), o = 0; o < n.length; o += 1)if (n[o] === e) return t[o]; if ("[object Array]" === h.call(e)) { for (n.push(e), l = new Array(e.length), t.push(l), o = 0; o < e.length; o += 1)l[o] = v(e[o], n, t, r, i); return n.pop(), t.pop(), l } if (e && e.toJSON && (e = e.toJSON()), "object" === d(e) && null !== e) { n.push(e), l = {}, t.push(l); var s, u = []; for (s in e) e.hasOwnProperty(s) && u.push(s); for (u.sort(), o = 0; o < u.length; o += 1)l[s = u[o]] = v(e[s], n, t, r, s); n.pop(), t.pop() } else l = e; return l } p.useLongestToken = !0, p.tokenize = u.tokenize, p.castInput = function (e) { var n = this.options, t = n.undefinedReplacement, n = n.stringifyReplacer, n = void 0 === n ? function (e, n) { return void 0 === n ? t : n } : n; return "string" == typeof e ? e : JSON.stringify(v(e, null, null, n), n, " ") }, p.equals = function (e, n) { return t.prototype.equals.call(p, e.replace(/,([\r\n])/g, "$1"), n.replace(/,([\r\n])/g, "$1")) }; var g = new t; function P(e) { var l = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {}, s = e.split(/\r\n|[\n\v\f\r\x85]/), u = e.match(/\r\n|[\n\v\f\r\x85]/g) || [], r = [], a = 0; function n() { var e = {}; for (r.push(e); a < s.length;) { var n = s[a]; if (/^(\-\-\-|\+\+\+|@@)\s/.test(n)) break; n = /^(?:Index:|diff(?: -r \w+)+)\s+(.+?)\s*$/.exec(n); n && (e.index = n[1]), a++ } for (i(e), i(e), e.hunks = []; a < s.length;) { var t = s[a]; if (/^(Index:|diff|\-\-\-|\+\+\+)\s/.test(t)) break; if (/^@@/.test(t)) e.hunks.push(function () { var e = a, n = s[a++].split(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/), t = { oldStart: +n[1], oldLines: void 0 === n[2] ? 1 : +n[2], newStart: +n[3], newLines: void 0 === n[4] ? 1 : +n[4], lines: [], linedelimiters: [] }; 0 === t.oldLines && (t.oldStart += 1); 0 === t.newLines && (t.newStart += 1); for (var r = 0, i = 0; a < s.length && !(0 === s[a].indexOf("--- ") && a + 2 < s.length && 0 === s[a + 1].indexOf("+++ ") && 0 === s[a + 2].indexOf("@@")); a++) { var o = 0 == s[a].length && a != s.length - 1 ? " " : s[a][0]; if ("+" !== o && "-" !== o && " " !== o && "\\" !== o) break; t.lines.push(s[a]), t.linedelimiters.push(u[a] || "\n"), "+" === o ? r++ : "-" === o ? i++ : " " === o && (r++, i++) } r || 1 !== t.newLines || (t.newLines = 0); i || 1 !== t.oldLines || (t.oldLines = 0); if (l.strict) { if (r !== t.newLines) throw new Error("Added line count did not match for hunk at line " + (e + 1)); if (i !== t.oldLines) throw new Error("Removed line count did not match for hunk at line " + (e + 1)) } return t }()); else { if (t && l.strict) throw new Error("Unknown line " + (a + 1) + " " + JSON.stringify(t)); a++ } } } function i(e) { var n, t, r = /^(---|\+\+\+)\s+(.*)$/.exec(s[a]); r && (n = "---" === r[1] ? "old" : "new", r = (t = r[2].split("\t", 2))[0].replace(/\\\\/g, "\\"), /^".*"$/.test(r) && (r = r.substr(1, r.length - 2)), e[n + "FileName"] = r, e[n + "Header"] = (t[1] || "").trim(), a++) } for (; a < s.length;)n(); return r } function y(e, n) { var t = 2 < arguments.length && void 0 !== arguments[2] ? arguments[2] : {}; if ("string" == typeof n && (n = P(n)), Array.isArray(n)) { if (1 < n.length) throw new Error("applyPatch only works with a single input."); n = n[0] } var r, i, o = e.split(/\r\n|[\n\v\f\r\x85]/), l = e.match(/\r\n|[\n\v\f\r\x85]/g) || [], s = n.hunks, u = t.compareLine || function (e, n, t, r) { return n === r }, a = 0, f = t.fuzzFactor || 0, d = 0, c = 0; for (var h = 0; h < s.length; h++) { for (var p = s[h], v = o.length - p.oldLines, g = 0, m = c + p.oldStart - 1, w = function (n, t, r) { var i = !0, o = !1, l = !1, s = 1; return function e() { if (i && !l) { if (o ? s++ : i = !1, n + s <= r) return s; l = !0 } if (!o) return l || (i = !0), t <= n - s ? -s++ : (o = !0, e()) } }(m, d, v); void 0 !== g; g = w())if (function (e, n) { for (var t = 0; t < e.lines.length; t++) { var r = e.lines[t], i = 0 < r.length ? r[0] : " ", r = 0 < r.length ? r.substr(1) : r; if (" " === i || "-" === i) { if (!u(n + 1, o[n], i, r) && f < ++a) return; n++ } } return 1 }(p, m + g)) { p.offset = c += g; break } if (void 0 === g) return !1; d = p.offset + p.oldStart + p.oldLines } for (var y = 0, L = 0; L < s.length; L++) { var x = s[L], S = x.oldStart + x.offset + y - 1; y += x.newLines - x.oldLines; for (var k = 0; k < x.lines.length; k++) { var b = x.lines[k], F = 0 < b.length ? b[0] : " ", N = 0 < b.length ? b.substr(1) : b, b = x.linedelimiters[k]; " " === F ? S++ : "-" === F ? (o.splice(S, 1), l.splice(S, 1)) : "+" === F ? (o.splice(S, 0, N), l.splice(S, 0, b), S++) : "\\" === F && ("+" === (F = x.lines[k - 1] ? x.lines[k - 1][0] : null) ? r = !0 : "-" === F && (i = !0)) } } if (r) for (; !o[o.length - 1];)o.pop(), l.pop(); else i && (o.push(""), l.push("\n")); for (var H = 0; H < o.length - 1; H++)o[H] = o[H] + l[H]; return o.join("") } function L(e, n, l, s, t, r, u) { void 0 === (u = u || {}).context && (u.context = 4); var a = m(l, s, u); function f(e) { return e.map(function (e) { return " " + e }) } a.push({ value: "", lines: [] }); for (var d = [], c = 0, h = 0, p = [], v = 1, g = 1, i = 0; i < a.length; i++)!function (e) { var n, t, r, i = a[e], o = i.lines || i.value.replace(/\n$/, "").split("\n"); i.lines = o, i.added || i.removed ? (c || (t = a[e - 1], c = v, h = g, t && (p = 0 < u.context ? f(t.lines.slice(-u.context)) : [], c -= p.length, h -= p.length)), p.push.apply(p, w(o.map(function (e) { return (i.added ? "+" : "-") + e }))), i.added ? g += o.length : v += o.length) : (c && (o.length <= 2 * u.context && e < a.length - 2 ? p.push.apply(p, w(f(o))) : (r = Math.min(o.length, u.context), p.push.apply(p, w(f(o.slice(0, r)))), n = { oldStart: c, oldLines: v - c + r, newStart: h, newLines: g - h + r, lines: p }, e >= a.length - 2 && o.length <= u.context && (t = /\n$/.test(l), r = /\n$/.test(s), e = 0 == o.length && p.length > n.oldLines, !t && e && 0 < l.length && p.splice(n.oldLines, 0, "\\ No newline at end of file"), (t || e) && r || p.push("\\ No newline at end of file")), d.push(n), h = c = 0, p = [])), v += o.length, g += o.length) }(i); return { oldFileName: e, newFileName: n, oldHeader: t, newHeader: r, hunks: d } } function x(e, n, t, r, i, o, l) { return function (e) { var n = []; e.oldFileName == e.newFileName && n.push("Index: " + e.oldFileName), n.push("==================================================================="), n.push("--- " + e.oldFileName + (void 0 === e.oldHeader ? "" : "\t" + e.oldHeader)), n.push("+++ " + e.newFileName + (void 0 === e.newHeader ? "" : "\t" + e.newHeader)); for (var t = 0; t < e.hunks.length; t++) { var r = e.hunks[t]; 0 === r.oldLines && --r.oldStart, 0 === r.newLines && --r.newStart, n.push("@@ -" + r.oldStart + "," + r.oldLines + " +" + r.newStart + "," + r.newLines + " @@"), n.push.apply(n, r.lines) } return n.join("\n") + "\n" }(L(e, n, t, r, i, o, l)) } function S(e, n) { if (n.length > e.length) return !1; for (var t = 0; t < n.length; t++)if (n[t] !== e[t]) return !1; return !0 } function k(e) { var n = function r(e) { var i = 0; var o = 0; e.forEach(function (e) { var n, t; "string" != typeof e ? (n = r(e.mine), t = r(e.theirs), void 0 !== i && (n.oldLines === t.oldLines ? i += n.oldLines : i = void 0), void 0 !== o && (n.newLines === t.newLines ? o += n.newLines : o = void 0)) : (void 0 === o || "+" !== e[0] && " " !== e[0] || o++, void 0 === i || "-" !== e[0] && " " !== e[0] || i++) }); return { oldLines: i, newLines: o } }(e.lines), t = n.oldLines, n = n.newLines; void 0 !== t ? e.oldLines = t : delete e.oldLines, void 0 !== n ? e.newLines = n : delete e.newLines } function b(e, n) { if ("string" != typeof e) return e; if (/^@@/m.test(e) || /^Index:/m.test(e)) return P(e)[0]; if (!n) throw new Error("Must provide a base reference or pass in a patch"); return L(void 0, void 0, n, e) } function F(e) { return e.newFileName && e.newFileName !== e.oldFileName } function N(e, n, t) { return n === t ? n : (e.conflict = !0, { mine: n, theirs: t }) } function H(e, n) { return e.oldStart < n.oldStart && e.oldStart + e.oldLines < n.oldStart } function C(e, n) { return { oldStart: e.oldStart, oldLines: e.oldLines, newStart: e.newStart + n, newLines: e.newLines, lines: e.lines } } function j(e, n, t, r) { var i = O(n), n = function (e, n) { var t = [], r = [], i = 0, o = !1, l = !1; for (; i < n.length && e.index < e.lines.length;) { var s = e.lines[e.index], u = n[i]; if ("+" === u[0]) break; if (o = o || " " !== s[0], r.push(u), i++, "+" === s[0]) for (l = !0; "+" === s[0];)t.push(s), s = e.lines[++e.index]; u.substr(1) === s.substr(1) ? (t.push(s), e.index++) : l = !0 } "+" === (n[i] || "")[0] && o && (l = !0); if (l) return t; for (; i < n.length;)r.push(n[i++]); return { merged: r, changes: t } }(t, i); n.merged ? (t = e.lines).push.apply(t, w(n.merged)) : z(e, r ? n : i, r ? i : n) } function z(e, n, t) { e.conflict = !0, e.lines.push({ conflict: !0, mine: n, theirs: t }) } function A(e, n, t) { for (; n.offset < t.offset && n.index < n.lines.length;) { var r = n.lines[n.index++]; e.lines.push(r), n.offset++ } } function E(e, n) { for (; n.index < n.lines.length;) { var t = n.lines[n.index++]; e.lines.push(t) } } function O(e) { for (var n = [], t = e.lines[e.index][0]; e.index < e.lines.length;) { var r = e.lines[e.index]; if ("-" === t && "+" === r[0] && (t = "+"), t !== r[0]) break; n.push(r), e.index++ } return n } function I(e) { return e.reduce(function (e, n) { return e && "-" === n[0] }, !0) } function $(e, n, t) { for (var r = 0; r < t; r++) { var i = n[n.length - t + r].substr(1); if (e.lines[e.index + r] !== " " + i) return } return e.index += t, 1 } g.tokenize = function (e) { return e.slice() }, g.join = g.removeEmpty = function (e) { return e }, e.Diff = t, e.applyPatch = y, e.applyPatches = function (e, i) { "string" == typeof e && (e = P(e)); var n = 0; !function t() { var r = e[n++]; if (!r) return i.complete(); i.loadFile(r, function (e, n) { return e ? i.complete(e) : (n = y(n, r, i), void i.patched(r, n, function (e) { return e ? i.complete(e) : void t() })) }) }() }, e.canonicalize = v, e.convertChangesToDMP = function (e) { for (var n, t, r = [], i = 0; i < e.length; i++)t = (n = e[i]).added ? 1 : n.removed ? -1 : 0, r.push([t, n.value]); return r }, e.convertChangesToXML = function (e) { for (var n = [], t = 0; t < e.length; t++) { var r = e[t]; r.added ? n.push("<ins>") : r.removed && n.push("<del>"), n.push(r.value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)), r.added ? n.push("</ins>") : r.removed && n.push("</del>") } return n.join("") }, e.createPatch = function (e, n, t, r, i, o) { return x(e, e, n, t, r, i, o) }, e.createTwoFilesPatch = x, e.diffArrays = function (e, n, t) { return g.diff(e, n, t) }, e.diffChars = function (e, n, t) { return r.diff(e, n, t) }, e.diffCss = function (e, n, t) { return f.diff(e, n, t) }, e.diffJson = function (e, n, t) { return p.diff(e, n, t) }, e.diffLines = m, e.diffSentences = function (e, n, t) { return a.diff(e, n, t) }, e.diffTrimmedLines = function (e, n, t) { return t = i(t, { ignoreWhitespace: !0 }), u.diff(e, n, t) }, e.diffWords = function (e, n, t) { return t = i(t, { ignoreWhitespace: !0 }), s.diff(e, n, t) }, e.diffWordsWithSpace = function (e, n, t) { return s.diff(e, n, t) }, e.merge = function (e, n, t) { e = b(e, t), n = b(n, t); var r = {}; (e.index || n.index) && (r.index = e.index || n.index), (e.newFileName || n.newFileName) && (F(e) ? F(n) ? (r.oldFileName = N(r, e.oldFileName, n.oldFileName), r.newFileName = N(r, e.newFileName, n.newFileName), r.oldHeader = N(r, e.oldHeader, n.oldHeader), r.newHeader = N(r, e.newHeader, n.newHeader)) : (r.oldFileName = e.oldFileName, r.newFileName = e.newFileName, r.oldHeader = e.oldHeader, r.newHeader = e.newHeader) : (r.oldFileName = n.oldFileName || e.oldFileName, r.newFileName = n.newFileName || e.newFileName, r.oldHeader = n.oldHeader || e.oldHeader, r.newHeader = n.newHeader || e.newHeader)), r.hunks = []; for (var i = 0, o = 0, l = 0, s = 0; i < e.hunks.length || o < n.hunks.length;) { var u, a = e.hunks[i] || { oldStart: 1 / 0 }, f = n.hunks[o] || { oldStart: 1 / 0 }; H(a, f) ? (r.hunks.push(C(a, l)), i++, s += a.newLines - a.oldLines) : H(f, a) ? (r.hunks.push(C(f, s)), o++, l += f.newLines - f.oldLines) : (function (e, n, t, r, i) { var o, l = { offset: n, lines: t, index: 0 }, s = { offset: r, lines: i, index: 0 }; A(e, l, s), A(e, s, l); for (; l.index < l.lines.length && s.index < s.lines.length;) { var u = l.lines[l.index], a = s.lines[s.index]; "-" !== u[0] && "+" !== u[0] || "-" !== a[0] && "+" !== a[0] ? "+" === u[0] && " " === a[0] ? (o = e.lines).push.apply(o, w(O(l))) : "+" === a[0] && " " === u[0] ? (o = e.lines).push.apply(o, w(O(s))) : "-" === u[0] && " " === a[0] ? j(e, l, s) : "-" === a[0] && " " === u[0] ? j(e, s, l, !0) : u === a ? (e.lines.push(u), l.index++, s.index++) : z(e, O(l), O(s)) : function (e, n, t) { var r, i = O(n), o = O(t); if (I(i) && I(o)) { if (S(i, o) && $(t, i, i.length - o.length)) return (t = e.lines).push.apply(t, w(i)); if (S(o, i) && $(n, o, o.length - i.length)) return (r = e.lines).push.apply(r, w(o)) } else if (function (e, n) { return e.length === n.length && S(e, n) }(i, o)) return (r = e.lines).push.apply(r, w(i)); z(e, i, o) }(e, l, s) } E(e, l), E(e, s), k(e) }(u = { oldStart: Math.min(a.oldStart, f.oldStart), oldLines: 0, newStart: Math.min(a.newStart + l, f.oldStart + s), newLines: 0, lines: [] }, a.oldStart, a.lines, f.oldStart, f.lines), o++, i++, r.hunks.push(u)) } return r }, e.parsePatch = P, e.structuredPatch = L, Object.defineProperty(e, "__esModule", { value: !0 }) });
|
|
</script>
|
|
<script id="cc">
|
|
const slideInLeft = [
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(1rem)'
|
|
},
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
}
|
|
]
|
|
const slideOutLeft = [
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
},
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(-1rem)'
|
|
},
|
|
]
|
|
const slideInRight = [
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(-1rem)'
|
|
},
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
}
|
|
]
|
|
const slideOutRight = [
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
},
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(1rem)'
|
|
},
|
|
]
|
|
const slideInDown = [
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateY(-1rem)'
|
|
},
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateY(0)'
|
|
},
|
|
]
|
|
const slideOutUp = [
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateY(0)'
|
|
},
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateY(-1rem)'
|
|
},
|
|
]
|
|
const cc = {
|
|
createNewArticle() {
|
|
if (floGlobals.isSubAdmin) {
|
|
const title = getRef('get_article_title').value.trim()
|
|
const setDefault = getRef('set_default_checkbox').checked
|
|
const plot = getRef('get_plot').value.trim()
|
|
const sectionTitles = parsePlot(plot)
|
|
let sections = []
|
|
if (sectionTitles) {
|
|
sections = sectionTitles.map(title => {
|
|
return {
|
|
id: floCrypto.randString(16, true),
|
|
title: title.trim(),
|
|
}
|
|
})
|
|
}
|
|
const uid = floCrypto.randString(16, true)
|
|
if (setDefault)
|
|
floGlobals.appObjects.cc['defaultArticle'] = uid
|
|
floGlobals.appObjects.cc['articleList'][uid] = {
|
|
title,
|
|
timestamp: Date.now(),
|
|
}
|
|
floGlobals.appObjects[uid] = {
|
|
public: true,
|
|
editors: [],
|
|
sections,
|
|
preview: {},
|
|
plot
|
|
}
|
|
Promise.all([
|
|
floCloudAPI.updateObjectData('cc'),
|
|
floCloudAPI.resetObjectData(uid)
|
|
])
|
|
.then((res) => {
|
|
closePopup()
|
|
notify('created article', 'success')
|
|
window.location.hash = `#/home?articleID=${uid}`
|
|
})
|
|
.catch(err => console.error(err))
|
|
} else {
|
|
notify('This action requires sub-admin privileges', 'error')
|
|
}
|
|
}
|
|
}
|
|
|
|
function parsePlot(plotText) {
|
|
let tstring = plotText.trim().replace(/\s+/g, " ") // collapse all whitespace to single whitespace
|
|
tstring = tstring.replace(/\)\s*\->\s*\(/g, ")->(");
|
|
|
|
tstring = tstring.split(")->("); // split string based on the delimiter
|
|
|
|
if (tstring.length > 0) {
|
|
if (tstring[0].trim()[0] == "(") {
|
|
tstring[0] = tstring[0].trim().slice(1)
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
|
|
let lastobj = tstring[tstring.length - 1];
|
|
let lastobjlen = lastobj.length;
|
|
|
|
if (lastobj.trim()[lastobjlen - 1] == ")") {
|
|
tstring[tstring.length - 1] = lastobj.trim().slice(0, lastobjlen - 1)
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
return tstring;
|
|
}
|
|
|
|
function setDefaultArticle() {
|
|
if (floGlobals.isSubAdmin) {
|
|
getConfirmation('Set as default article?').then(res => {
|
|
if (res) {
|
|
floGlobals.appObjects.cc['defaultArticle'] = floGlobals.currentArticle.id
|
|
floCloudAPI.updateObjectData('cc')
|
|
.then((res) => {
|
|
notify('Set current article as default', 'success')
|
|
})
|
|
.catch(err => console.error(err))
|
|
}
|
|
})
|
|
} else {
|
|
notify('This action requires sub-admin privileges', 'error')
|
|
}
|
|
}
|
|
|
|
function renderPreview() {
|
|
const composedDocumentStructure = formatSelectedContent()
|
|
let frag = document.createDocumentFragment()
|
|
|
|
for (const section in composedDocumentStructure) {
|
|
frag.append(createElement('h3', {
|
|
textContent: floGlobals.currentArticle.sections[section].title
|
|
}))
|
|
composedDocumentStructure[section].forEach(({ content, uid }) => {
|
|
const group = createElement('section', {
|
|
className: 'preview-group',
|
|
attributes: { 'data-section-id': section, 'data-uid': uid },
|
|
innerHTML: content
|
|
})
|
|
group.append(createElement('div', {
|
|
className: 'preview-group__buttons flex align-center',
|
|
innerHTML: `
|
|
<button class="formatting-button move-up" title="Move up">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>
|
|
</button>
|
|
<button class="formatting-button remove-group" title="Remove from exporting">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>
|
|
</button>
|
|
`
|
|
}))
|
|
frag.append(group)
|
|
})
|
|
}
|
|
getRef('preview__title').textContent = floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title
|
|
getRef('preview__body').innerHTML = ''
|
|
getRef('preview__body').append(frag)
|
|
window.location.hash = `#/preview?articleID=${floGlobals.currentArticle.id}`
|
|
}
|
|
|
|
function getReadingTime(content) {
|
|
const wpm = 250;
|
|
const words = content.trim().split(/\s+/).length;
|
|
return Math.floor(words / wpm);
|
|
}
|
|
|
|
|
|
function downloadHTML(string, options = {}) {
|
|
const { title } = options
|
|
const element = html.node`<a href="${`${`data:text/html;charset=utf-8, ${encodeURIComponent(string)}`}`}" download="${`${title ? title : 'article'}.html`}" class="hide"></a>`
|
|
document.body.appendChild(element);
|
|
element.click();
|
|
document.body.removeChild(element);
|
|
}
|
|
|
|
function getCleanExportContent() {
|
|
const exportContent = getRef('preview__body').cloneNode(true)
|
|
exportContent.querySelectorAll('.preview-group').forEach(section => {
|
|
section.lastElementChild.remove()
|
|
section.replaceWith(...section.childNodes)
|
|
})
|
|
return DOMPurify.sanitize(exportContent.innerHTML, { FORBID_ATTR: ['style'], ADD_ATTR: ['target'] })
|
|
}
|
|
|
|
function exportSelection() {
|
|
const bodyTemplate = getRef('body_template').content.cloneNode(true)
|
|
const articleTitle = floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title
|
|
bodyTemplate.querySelector('#exported_title').textContent = articleTitle
|
|
bodyTemplate.querySelector('#exported_time').textContent = `Exported on ${getFormattedTime(Date.now())}`
|
|
bodyTemplate.querySelector('#export_body').innerHTML = getCleanExportContent()
|
|
let allContributors = new Set()
|
|
selectedContent.forEach(({ contributors }) => {
|
|
contributors.forEach(id => allContributors.add(id))
|
|
});
|
|
const frag = document.createDocumentFragment()
|
|
allContributors.forEach(id => {
|
|
frag.append(html.node`<div class="contributor">${id}</div>`)
|
|
})
|
|
bodyTemplate.querySelector('#article_contributors').append(frag)
|
|
const readingTime = getReadingTime(bodyTemplate.querySelector('#export_body').textContent)
|
|
bodyTemplate.querySelector('#reading_time').textContent = `${readingTime} min read`
|
|
let bodyAttributes = ''
|
|
if (!floGlobals.isSubAdmin) {
|
|
bodyTemplate.querySelector('header button').remove()
|
|
}
|
|
const bodyHTML = createElement('div')
|
|
bodyHTML.append(bodyTemplate)
|
|
const headTemplate = getRef('head_template').content.cloneNode(true)
|
|
headTemplate.querySelector('title').textContent = articleTitle
|
|
const headHTML = createElement('div')
|
|
headHTML.append(headTemplate)
|
|
const finalMarkup = `
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>${headHTML.innerHTML}<\/head>
|
|
<body data-theme="light" ${bodyAttributes}>
|
|
${bodyHTML.innerHTML}
|
|
<\/body>
|
|
<\/html>
|
|
`
|
|
downloadHTML(finalMarkup, { title: articleTitle })
|
|
}
|
|
function goHome() {
|
|
window.location.hash = `#/home?articleID=${pagesData.params.articleID}`
|
|
}
|
|
function sharePreview() {
|
|
if (floGlobals.isSubAdmin) {
|
|
let allContributors = new Set()
|
|
selectedContent.forEach(({ contributors }) => {
|
|
contributors.forEach(id => allContributors.add(id))
|
|
});
|
|
if (floGlobals.appObjects[pagesData.params.articleID]?.preview?.id) {
|
|
floGlobals.appObjects[pagesData.params.articleID].preview.content = DOMPurify.sanitize(getRef('preview__body').innerHTML, { FORBID_ATTR: ['style'], ADD_ATTR: ['target'] })
|
|
} else {
|
|
floGlobals.appObjects[pagesData.params.articleID].preview = {
|
|
uid: floCrypto.randString(16, true),
|
|
content: DOMPurify.sanitize(getRef('preview__body').innerHTML, { FORBID_ATTR: ['style'], ADD_ATTR: ['target'] }),
|
|
contributors: [...allContributors]
|
|
}
|
|
}
|
|
floCloudAPI.updateObjectData(pagesData.params.articleID)
|
|
.then(() => {
|
|
notify('Created preview', 'success')
|
|
const uid = floGlobals.appObjects[pagesData.params.articleID].preview.uid
|
|
history.replaceState(null, null, `#/preview?articleID=${pagesData.params.articleID}&uid=${uid}`)
|
|
if (window.navigator.share) {
|
|
navigator.share({
|
|
title: "Share article preview",
|
|
url: window.location.href
|
|
})
|
|
} else {
|
|
getRef('shared_url').value = window.location.href
|
|
openPopup('share_popup')
|
|
}
|
|
})
|
|
.catch(err => notify(err, 'error'))
|
|
} else {
|
|
notify('This action requires sub-admin privileges', 'error')
|
|
}
|
|
}
|
|
|
|
function publishArticle() {
|
|
if (floGlobals.isSubAdmin) {
|
|
getConfirmation('Request publishing?', {
|
|
message: 'Send this article as publishing candidate on RanchiMall TImes',
|
|
confirmText: 'Request'
|
|
})
|
|
.then(res => {
|
|
if (res) {
|
|
const content = getCleanExportContent();
|
|
const readTime = getReadingTime(createElement('div', { innerHTML: content }).textContent)
|
|
let allContributors = new Set()
|
|
selectedContent.forEach(({ contributors }) => {
|
|
contributors.forEach(id => allContributors.add(id))
|
|
});
|
|
floCloudAPI.sendGeneralData({
|
|
articleID: pagesData.params.articleID,
|
|
title: floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title,
|
|
content,
|
|
readTime,
|
|
contributors: [...allContributors],
|
|
}, 'publishing_requests', {
|
|
application: 'RM_Times',
|
|
receiverID: 'FF5pewfsJxyrCvg8a2C8VXefeyVvKvQxmF'
|
|
})
|
|
.then(res => {
|
|
console.log(res)
|
|
notify('Publication request sent', 'success')
|
|
})
|
|
.catch(err => notify(err, 'error'))
|
|
}
|
|
})
|
|
} else {
|
|
notify('This action requires sub-admin privileges', 'error')
|
|
}
|
|
}
|
|
|
|
getRef('preview_page').addEventListener('click', e => {
|
|
if (e.target.closest('.move-up')) {
|
|
const currentElement = e.target.closest('.preview-group')
|
|
const previousSibling = currentElement.previousElementSibling
|
|
if (previousSibling?.dataset.sectionId === currentElement.dataset.sectionId) {
|
|
const clone = currentElement.cloneNode(true)
|
|
previousSibling.before(clone)
|
|
currentElement.remove()
|
|
}
|
|
} else if (e.target.closest('.move-down')) {
|
|
const currentElement = e.target.closest('.preview-group')
|
|
const nextSibling = currentElement.nextElementSibling
|
|
if (nextSibling?.dataset.sectionId === currentElement.dataset.sectionId) {
|
|
const clone = currentElement.cloneNode(true)
|
|
nextSibling.after(clone)
|
|
currentElement.remove()
|
|
}
|
|
} else if (e.target.closest('.remove-group')) {
|
|
const currentElement = e.target.closest('.preview-group')
|
|
const uid = currentElement.dataset.uid
|
|
if (floGlobals.currentArticle.id) {
|
|
getRef('article_wrapper').querySelector(`.content-card[data-uid="${uid}"] sm-checkbox`).checked = false
|
|
}
|
|
if ((!currentElement.nextElementSibling || currentElement.nextElementSibling.tagName === 'H3') && currentElement.previousElementSibling.tagName === 'H3') {
|
|
currentElement.previousElementSibling.remove()
|
|
}
|
|
currentElement.remove()
|
|
}
|
|
})
|
|
|
|
let activeEntry
|
|
getRef('article_wrapper').addEventListener("focusin", e => {
|
|
if (e.target.closest('.content__area')) {
|
|
const target = e.target.closest('.content__area')
|
|
activeEntry = target.closest('.content-card')
|
|
document.addEventListener('selectionchange', detectFormatting)
|
|
if (target.childNodes[0] === undefined) {
|
|
target.innerHTML = `<p><br/></p>`
|
|
}
|
|
childObserver.observe(target, {
|
|
childList: true,
|
|
})
|
|
target.addEventListener('input', checkEntry)
|
|
target.addEventListener('paste', checkEntry)
|
|
}
|
|
})
|
|
getRef('article_wrapper').addEventListener("focusout", e => {
|
|
if (e.target.closest('.content__area')) {
|
|
const target = e.target.closest('.content__area')
|
|
normalizeText(e.target.closest('.content__area'))
|
|
document.removeEventListener('selectionchange', detectFormatting)
|
|
target.removeEventListener('input', checkEntry)
|
|
target.removeEventListener('paste', checkEntry)
|
|
const selection = window.getSelection()
|
|
if (!e.relatedTarget?.closest('#text_toolbar')) {
|
|
getRef('text_toolbar').classList.add('hide-completely')
|
|
childObserver.disconnect()
|
|
activeEntry = undefined
|
|
}
|
|
}
|
|
})
|
|
const childObserver = new MutationObserver((mutations, observer) => {
|
|
mutations.forEach(mutation => {
|
|
if (mutation.type === 'childList') {
|
|
if (mutation.removedNodes.length && mutation.target.childNodes[0] === undefined) {
|
|
observer.disconnect()
|
|
mutation.target.innerHTML = `<p><br/></p>`
|
|
childObserver.observe(mutation.target, {
|
|
childList: true,
|
|
})
|
|
}
|
|
}
|
|
})
|
|
})
|
|
document.execCommand("defaultParagraphSeparator", false, "p");
|
|
function formatDoc(sCmd, sValue) {
|
|
document.execCommand(sCmd, false, sValue);
|
|
checkEntry()
|
|
}
|
|
function incScore(value) {
|
|
let currentScore = parseFloat(getRef('update_score_field').value)
|
|
if (!currentScore) {
|
|
currentScore = 0
|
|
}
|
|
if (currentScore + value <= 100) {
|
|
currentScore += value;
|
|
getRef('update_score_field').value = parseFloat(currentScore.toFixed(1))
|
|
}
|
|
else {
|
|
notify(`You can't give score more than 100.`, 'error')
|
|
}
|
|
}
|
|
function updateScore() {
|
|
const newScore = parseFloat(getRef('update_score_field').value)
|
|
updateGenData(floGlobals.versionHistory.currentEntry, { tag: newScore })
|
|
document.querySelectorAll(`[data-vector-clock="${floGlobals.versionHistory.currentEntry}"] .content__score`).forEach(scoreElem => scoreElem.textContent = newScore)
|
|
document.querySelectorAll(`[data-vector-clock="${floGlobals.versionHistory.currentEntry}"] .content__score`).forEach(scoreElem => newScore > 0 ? scoreElem.parentNode.classList.add('score-button--filled') : scoreElem.parentNode.classList.remove('score-button--filled'))
|
|
floCloudAPI.tagApplicationData(floGlobals.versionHistory.currentEntry, newScore).then(res => {
|
|
notify('Score updated', 'success')
|
|
closePopup()
|
|
}).catch(err => notify(err, 'error'))
|
|
}
|
|
floGlobals.currentArticle = {}
|
|
const sectionIntersectionObserver = new IntersectionObserver((entries, observer) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const section = entry.target.parentNode
|
|
const frag = document.createDocumentFragment();
|
|
const arrayOfElements = floGlobals.currentArticle.sections[section.dataset.sectionId].uniqueEntries
|
|
const startIndex = floGlobals.isSubAdmin ? section.children.length : section.children.length - 1
|
|
arrayOfElements.slice(startIndex, startIndex + 5).forEach(elem => frag.append(render.contentCard(elem)))
|
|
entry.target.parentNode.append(frag)
|
|
observer.unobserve(entry.target)
|
|
}
|
|
})
|
|
})
|
|
const sectionMutationObserver = new MutationObserver(mutationList => {
|
|
mutationList.forEach(mutation => {
|
|
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
sectionIntersectionObserver.observe(mutation.target.lastElementChild)
|
|
}
|
|
})
|
|
})
|
|
let isMobileView = false
|
|
const mobileQuery = window.matchMedia('(max-width: 40rem)')
|
|
function handleMobileChange(e) {
|
|
if (e.matches) {
|
|
// Mobile view
|
|
isMobileView = true
|
|
document.querySelectorAll('.article-section').forEach(section => {
|
|
sectionMutationObserver.observe(section, { childList: true })
|
|
})
|
|
} else {
|
|
// Desktop view
|
|
isMobileView = false
|
|
sectionMutationObserver.disconnect()
|
|
sectionIntersectionObserver.disconnect()
|
|
}
|
|
}
|
|
mobileQuery.addListener(handleMobileChange)
|
|
handleMobileChange(mobileQuery)
|
|
function getScoreElement(score) {
|
|
let scoreElement
|
|
if (floGlobals.isSubAdmin) {
|
|
scoreElement = html.node`<button class="score-button" title="Score this content"></button>`
|
|
} else {
|
|
scoreElement = html.node`<div class="flex align-center" title="Score"></div>`
|
|
}
|
|
if (score > 0) {
|
|
scoreElement.classList.add('score-button--filled')
|
|
}
|
|
scoreElement.innerHTML = `
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><path d="M0,0h24v24H0V0z" fill="none"/><path d="M0,0h24v24H0V0z" fill="none"/></g><g><g><polygon opacity=".3" points="12,15.4 8.24,17.67 9.24,13.39 5.92,10.51 10.3,10.13 12,6.1 13.71,10.14 18.09,10.52 14.77,13.4 15.77,17.68"/><path d="M22,9.24l-7.19-0.62L12,2L9.19,8.63L2,9.24l5.46,4.73L5.82,21L12,17.27L18.18,21l-1.63-7.03L22,9.24z M12,15.4l-3.76,2.27 l1-4.28l-3.32-2.88l4.38-0.38L12,6.1l1.71,4.04l4.38,0.38l-3.32,2.88l1,4.28L12,15.4z"/></g></g></svg>
|
|
<div class="content__score">${score}</div>
|
|
`
|
|
return scoreElement
|
|
}
|
|
const render = {
|
|
article(id) {
|
|
floGlobals.currentArticle.id = id
|
|
parseArticleData()
|
|
const { title } = floGlobals.appObjects.cc.articleList[id]
|
|
const { writer, sections } = floGlobals.currentArticle
|
|
const frag = document.createDocumentFragment()
|
|
let index = 0
|
|
for (const sectionID in sections) {
|
|
frag.append(render.section(sectionID, sections[sectionID], index))
|
|
index += 1
|
|
}
|
|
getRef('current_article_title').textContent = title
|
|
getRef('article_wrapper').innerHTML = ''
|
|
getRef('article_wrapper').append(frag)
|
|
render.sectionOutline()
|
|
},
|
|
articleLink(details, isDefaultArticle, ref) {
|
|
const { uid, timestamp, title } = details
|
|
return html.for(ref, uid)`<a class="${`article-link flex interact ${isDefaultArticle ? 'default-article' : ''}`}" title=${isDefaultArticle ? 'Actively written article' : ''} href="${`#/home?articleID=${uid}`}">${title}</a>`
|
|
},
|
|
contentCard(id, version = 0) {
|
|
const { html, contributors, score = 0, vectorClock } = getIterationDetails(id)
|
|
const clone = getRef('content_card_template').content.cloneNode(true).firstElementChild;
|
|
clone.dataset.uid = id
|
|
clone.dataset.vectorClock = vectorClock
|
|
if (!floGlobals.isSubAdmin) {
|
|
clone.querySelector('.content__area').setAttribute('contentEditable', true)
|
|
}
|
|
clone.querySelector('.content__area').innerHTML = DOMPurify.sanitize(html, { FORBID_ATTR: ['style'], ADD_ATTR: ['target'] })
|
|
let noOfContributors = 0
|
|
let latestContributor
|
|
for (const contributor in contributors) {
|
|
noOfContributors++
|
|
latestContributor = contributor
|
|
}
|
|
clone.querySelector('.content__author').innerHTML = `<div>${latestContributor}</div> ${noOfContributors === 1 ? '' : `<div> and ${noOfContributors - 1} more`}</div>`
|
|
clone.querySelector('.content__options').append(getScoreElement(score));
|
|
return clone
|
|
},
|
|
historyEntry(details, oldText) {
|
|
const { editor, timestamp, plainText, score, vectorClock } = details
|
|
const clone = getRef('history_entry_template').content.cloneNode(true).firstElementChild;
|
|
clone.dataset.vectorClock = vectorClock
|
|
clone.querySelector('.entry__time').textContent = getFormattedTime(timestamp)
|
|
clone.querySelector('.entry__time').after(getScoreElement(score))
|
|
clone.querySelector('.entry__author').textContent = editor
|
|
if (oldText !== '') {
|
|
const frag = document.createDocumentFragment()
|
|
Diff.diffWords(oldText, plainText).forEach((part) => {
|
|
if (part.hasOwnProperty('added') || part.hasOwnProperty('removed')) {
|
|
const type = part.added ? 'added' :
|
|
part.removed ? 'removed' : '';
|
|
frag.append(
|
|
createElement('span', {
|
|
textContent: part.value,
|
|
className: type
|
|
})
|
|
);
|
|
} else {
|
|
frag.append(
|
|
document.createTextNode(part.value)
|
|
)
|
|
}
|
|
});
|
|
clone.querySelector('.entry__changes').append(frag)
|
|
} else {
|
|
clone.querySelector('.entry__changes').textContent = plainText
|
|
}
|
|
return clone
|
|
},
|
|
section(sectionID, { title, uniqueEntries }) {
|
|
const sortByScore = getRef('sort_content_list').value === 'score'
|
|
const section = getRef('section_template').content.cloneNode(true)
|
|
const frag = document.createDocumentFragment()
|
|
section.children[0].dataset.sectionId = sectionID
|
|
section.children[1].dataset.sectionId = sectionID
|
|
if (floGlobals.isSubAdmin) {
|
|
section.querySelector('.content-card--empty').remove()
|
|
}
|
|
section.querySelector('.section-title').textContent = title
|
|
floGlobals.currentArticle.sections[sectionID].uniqueEntries.sort((a, b) => {
|
|
const arrayA = floGlobals.currentArticle.uniqueEntries[a].iterations
|
|
const arrayB = floGlobals.currentArticle.uniqueEntries[b].iterations
|
|
return getGenData(arrayB[arrayB.length - 1])[sortByScore ? 'score' : 'timestamp'] - getGenData(arrayA[arrayA.length - 1])[sortByScore ? 'score' : 'timestamp']
|
|
}).slice(0, maxCardsPerSection).forEach(entry => {
|
|
const contentCard = render.contentCard(entry)
|
|
if (contentCard)
|
|
frag.append(contentCard)
|
|
})
|
|
if (floGlobals.currentArticle.sections[sectionID].uniqueEntries.length > maxCardsPerSection) {
|
|
section.querySelector('.heading').append(createElement('button', {
|
|
className: 'button see-more hide-on-mobile',
|
|
textContent: `See ${floGlobals.currentArticle.sections[sectionID].uniqueEntries.length - maxCardsPerSection} more entries`
|
|
}))
|
|
}
|
|
if (isMobileView)
|
|
sectionMutationObserver.observe(section.querySelector('.article-section'), { childList: true })
|
|
section.querySelector('.article-section').append(frag)
|
|
headingIntersectionObserver.observe(section.children[1])
|
|
return section
|
|
},
|
|
sectionCard(details) {
|
|
const { title, id } = details
|
|
return html.node`
|
|
<div class="section-card flex align-center" data-section-id="${id}">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M9,18h12v-2H9V18z M3,6v2h18V6H3z M9,13h12v-2H9V13z"/></g></svg>
|
|
<input placeholder="Section title" value="${title}"/>
|
|
</div>`
|
|
},
|
|
sectionOutline() {
|
|
const frag = document.createDocumentFragment()
|
|
getRef('article_outline').innerHTML = ''
|
|
floGlobals.appObjects[floGlobals.currentArticle.id].sections.forEach(section => {
|
|
frag.append(createElement('button', {
|
|
attributes: { 'data-section-id': section.id },
|
|
className: 'outline-button',
|
|
textContent: section.title,
|
|
}))
|
|
})
|
|
getRef('article_outline').append(frag)
|
|
}
|
|
}
|
|
|
|
function parseArticleData() {
|
|
const { sections, editors, public } = floGlobals.appObjects[floGlobals.currentArticle.id]
|
|
const generalData = floGlobals.generalData[`${floGlobals.currentArticle.id}_gd|${floGlobals.adminID}|${floGlobals.application}`]
|
|
floGlobals.currentArticle['sections'] = {}
|
|
sections.forEach(({ id, title }) => {
|
|
floGlobals.currentArticle['sections'][id] = {
|
|
expanded: false,
|
|
title,
|
|
uniqueEntries: new Set()
|
|
}
|
|
})
|
|
floGlobals.currentArticle['uniqueEntries'] = {}
|
|
for (const key in generalData) {
|
|
const { section, origin } = generalData[key].message
|
|
if (!floGlobals.currentArticle.uniqueEntries.hasOwnProperty(origin)) { // check if general data has origin that's already defined
|
|
floGlobals.currentArticle.uniqueEntries[origin] = {
|
|
iterations: []
|
|
}
|
|
}
|
|
if (floGlobals.currentArticle.sections[section]) {
|
|
floGlobals.currentArticle.sections[section].uniqueEntries.add(origin)
|
|
}
|
|
floGlobals.currentArticle.uniqueEntries[origin]['iterations'].push(generalData[key].vectorClock)
|
|
}
|
|
for (const sectionID in floGlobals.currentArticle.sections) {
|
|
floGlobals.currentArticle.sections[sectionID].uniqueEntries = [...floGlobals.currentArticle.sections[sectionID].uniqueEntries].reverse()
|
|
}
|
|
for (const entry in floGlobals.currentArticle.uniqueEntries) {
|
|
floGlobals.currentArticle.uniqueEntries[entry]['iterations'].sort((a, b) => getGenData(a).timestamp - getGenData(b).timestamp)
|
|
}
|
|
}
|
|
|
|
function getGenData(vectorClock) {
|
|
const { message: { section, origin, plainText, html, hash }, senderID, time, tag } = floGlobals.generalData[`${floGlobals.currentArticle.id}_gd|${floGlobals.adminID}|${floGlobals.application}`][vectorClock]
|
|
let tempDiv
|
|
if (!plainText) {
|
|
// convert html to innerText
|
|
tempDiv = createElement('div', {
|
|
innerHTML: html
|
|
})
|
|
}
|
|
return {
|
|
plainText: plainText ? plainText : tempDiv.innerText,
|
|
html,
|
|
editor: senderID,
|
|
hash,
|
|
score: tag ? tag : 0,
|
|
timestamp: time,
|
|
vectorClock,
|
|
}
|
|
}
|
|
function updateGenData(vectorClock, propsToUpdate = {}) {
|
|
floGlobals.generalData[`${floGlobals.currentArticle.id}_gd|${floGlobals.adminID}|${floGlobals.application}`][vectorClock] = { ...floGlobals.generalData[`${floGlobals.currentArticle.id}_gd|${floGlobals.adminID}|${floGlobals.application}`][vectorClock], ...propsToUpdate }
|
|
}
|
|
|
|
const checkEntry = debounce(e => {
|
|
const contentCard = activeEntry
|
|
const contentArea = contentCard.querySelector('.content__area')
|
|
const uid = contentCard.dataset.uid
|
|
const isUniqueEntry = contentArea.dataset.type === 'origin'
|
|
if (contentArea.innerText.trim() === '') {
|
|
contentCard.querySelector('.submit-entry').classList.add('hide-completely')
|
|
} else {
|
|
const cleanHTML = DOMPurify.sanitize(contentArea.innerHTML.split('\n').map(v => v.trim()).filter(v => v).join('\n'), { FORBID_ATTR: ['style'], ADD_ATTR: ['target'] })
|
|
const hash = Crypto.SHA256(cleanHTML)
|
|
let previousHash
|
|
if (!isUniqueEntry) {
|
|
({ hash: previousHash = '' } = getIterationDetails(uid))
|
|
} else {
|
|
previousHash = ''
|
|
}
|
|
if (previousHash !== hash)
|
|
contentCard.querySelector('.submit-entry').classList.remove('hide-completely')
|
|
else
|
|
contentCard.querySelector('.submit-entry').classList.add('hide-completely')
|
|
}
|
|
|
|
}, 300)
|
|
getRef('article_wrapper').addEventListener('click', e => {
|
|
if (e.target.closest('.submit-entry')) {
|
|
const submitButton = e.target.closest('.submit-entry')
|
|
const contentCard = e.target.closest('.content-card')
|
|
const contentArea = contentCard.querySelector('.content__area')
|
|
const uid = contentCard.dataset.uid
|
|
const isUniqueEntry = contentArea.dataset.type === 'origin'
|
|
const parentSection = contentCard.closest('.article-section')
|
|
const sectionID = parentSection.dataset.sectionId
|
|
const plainText = contentArea.innerText.trim()
|
|
if (plainText === '') return
|
|
if (contentArea.firstChild.nodeType === Node.TEXT_NODE) {
|
|
const clone = contentArea.firstChild.cloneNode(true)
|
|
const p = createElement('p')
|
|
p.append(clone)
|
|
contentArea.firstChild.remove()
|
|
contentArea.prepend(p)
|
|
}
|
|
const cleanHTML = DOMPurify.sanitize(contentArea.innerHTML.split('\n').map(v => v.trim()).filter(v => v).join('\n'), { FORBID_ATTR: ['style'], ADD_ATTR: ['target'] })
|
|
const hash = Crypto.SHA256(cleanHTML)
|
|
let previousVersion, previousHash
|
|
if (!isUniqueEntry) {
|
|
({ html: previousVersion, hash: previousHash = '', contributors } = getIterationDetails(uid))
|
|
} else {
|
|
previousHash = ''
|
|
}
|
|
if (previousHash !== hash) {
|
|
const timestamp = Date.now()
|
|
const entry = {
|
|
section: sectionID,
|
|
origin: isUniqueEntry ? floCrypto.randString(16, true) : uid,
|
|
plainText,
|
|
html: cleanHTML,
|
|
hash
|
|
}
|
|
submitButton.innerHTML = `<sm-spinner></sm-spinner>`
|
|
submitButton.disabled = true
|
|
floCloudAPI.sendGeneralData(entry, `${floGlobals.currentArticle.id}_gd`)
|
|
.then((res) => {
|
|
// Add result to general data
|
|
floGlobals.generalData[`${floGlobals.currentArticle.id}_gd|${floGlobals.adminID}|${floGlobals.application}`][res.vectorClock] = { ...res, message: entry }
|
|
submitButton.classList.add('hide-completely')
|
|
notify('Content submitted', 'success')
|
|
if (isUniqueEntry) {
|
|
contentArea.innerHTML = ''
|
|
floGlobals.currentArticle.sections[sectionID].uniqueEntries.push(entry.origin)
|
|
floGlobals.currentArticle.uniqueEntries[entry.origin] = { iterations: [res.vectorClock] }
|
|
// Insert new content card based on set filter
|
|
const newCard = render.contentCard(entry.origin)
|
|
if (getRef('sort_content_list').value === 'time') {
|
|
parentSection.firstElementChild.after(newCard)
|
|
newCard.animate(slideInRight, {
|
|
duration: 300,
|
|
fill: 'forwards',
|
|
easing: 'ease'
|
|
})
|
|
const width = newCard.getBoundingClientRect().width
|
|
const siblings = Array.from(newCard.parentNode.children)
|
|
const nextSiblings = siblings.slice(siblings.indexOf(newCard) + 1)
|
|
nextSiblings.forEach(elem => {
|
|
elem.animate([
|
|
{
|
|
transform: `translateX(-${width}px)`
|
|
},
|
|
{
|
|
transform: 'translateX(0)'
|
|
},
|
|
], {
|
|
duration: 200,
|
|
fill: 'forwards',
|
|
easing: 'ease'
|
|
})
|
|
.onfinish = (e) => {
|
|
e.target.cancel()
|
|
}
|
|
})
|
|
} else {
|
|
parentSection.append(newCard)
|
|
}
|
|
} else {
|
|
let noOfContributors = 0
|
|
for (const contributor in contributors) {
|
|
noOfContributors++
|
|
if (noOfContributors === 2)
|
|
break
|
|
}
|
|
if (noOfContributors < 2 && !contributors.hasOwnProperty(myFloID)) {
|
|
contentCard.querySelector('.content__author').textContent = `${myFloID} and 1 more`
|
|
}
|
|
contentCard.querySelector('.content__score').textContent = 0
|
|
contentCard.querySelector('.content__score').parentNode.classList.remove('score-button--filled')
|
|
floGlobals.currentArticle.uniqueEntries[entry.origin].iterations.push(res.vectorClock)
|
|
}
|
|
})
|
|
.catch(err => console.log(err))
|
|
.finally(() => {
|
|
submitButton.textContent = 'Submit'
|
|
submitButton.disabled = false
|
|
})
|
|
} else {
|
|
notify("Duplicate entry!", 'error')
|
|
}
|
|
} else if (e.target.closest('.version-history-button')) {
|
|
const entryUid = e.target.closest('.content-card').dataset.uid
|
|
if (floGlobals.versionHistory.isOpen && entryUid === floGlobals.versionHistory.entryUid)
|
|
hideVersionHistory()
|
|
else
|
|
showVersionHistory(entryUid)
|
|
} else if (e.target.closest('.content__contributors')) {
|
|
const entryUid = e.target.closest('.content-card').dataset.uid
|
|
const { contributors } = getIterationDetails(entryUid)
|
|
const frag = document.createDocumentFragment()
|
|
getRef('contributor_list').innerHTML = ''
|
|
for (const floID in contributors) {
|
|
const contributor = getRef('contributor_template').content.cloneNode(true)
|
|
contributor.querySelector('.contributor__id').textContent = floID
|
|
contributor.querySelector('.contributor__time').textContent = `Last edited: ${getFormattedTime(contributors[floID])}`
|
|
frag.prepend(contributor)
|
|
}
|
|
getRef('contributor_list').append(frag)
|
|
openPopup('contributors_popup')
|
|
} else if (e.target.closest('.see-more')) {
|
|
const target = e.target.closest('.see-more')
|
|
const sectionID = target.parentNode.dataset.sectionId
|
|
if (floGlobals.currentArticle.sections[sectionID].expanded) {
|
|
target.textContent = `See ${floGlobals.currentArticle.sections[sectionID].uniqueEntries.length - maxCardsPerSection} more entries`;
|
|
[...target.parentNode.nextElementSibling.children].slice(3).forEach(card => {
|
|
card.querySelector('sm-checkbox').checked = false
|
|
card.remove()
|
|
})
|
|
floGlobals.currentArticle.sections[sectionID].expanded = false
|
|
} else {
|
|
target.textContent = 'See less'
|
|
floGlobals.currentArticle.sections[sectionID].uniqueEntries.slice(maxCardsPerSection).forEach(entry => {
|
|
const contentCard = render.contentCard(entry)
|
|
if (contentCard)
|
|
target.parentNode.nextElementSibling.append(contentCard)
|
|
})
|
|
floGlobals.currentArticle.sections[sectionID].expanded = true
|
|
}
|
|
} else if (e.target.closest('.score-button') && floGlobals.isSubAdmin) {
|
|
floGlobals.versionHistory.currentEntry = e.target.closest('.content-card').dataset.vectorClock
|
|
getRef('update_score_field').value = e.target.closest('.score-button').children[1].textContent
|
|
openPopup('scoring_popup')
|
|
}
|
|
})
|
|
getRef('version_timeline').addEventListener('click', e => {
|
|
if (e.target.closest('.score-button') && floGlobals.isSubAdmin) {
|
|
floGlobals.versionHistory.currentEntry = e.target.closest('.history-entry').dataset.vectorClock
|
|
getRef('update_score_field').value = e.target.closest('.score-button').children[1].textContent
|
|
openPopup('scoring_popup')
|
|
}
|
|
})
|
|
getRef('article_wrapper').addEventListener("paste", e => {
|
|
const paste = (event.clipboardData || window.clipboardData).getData('text');
|
|
|
|
const selection = window.getSelection();
|
|
if (!selection.rangeCount) return false;
|
|
selection.deleteFromDocument();
|
|
selection.getRangeAt(0).insertNode(document.createTextNode(paste));
|
|
|
|
event.preventDefault();
|
|
})
|
|
const selectedContent = new Map()
|
|
let isContentSelected = false
|
|
getRef('article_wrapper').addEventListener("change", e => {
|
|
if (e.target.closest('sm-checkbox')) {
|
|
const contentCard = e.target.closest('.content-card')
|
|
const contentID = contentCard.dataset.uid
|
|
contentCard.classList.toggle('selected')
|
|
if (!floGlobals.isSubAdmin)
|
|
contentCard.querySelector('.content__area').toggleAttribute('contenteditable')
|
|
if (selectedContent.has(contentID)) {
|
|
selectedContent.delete(contentID)
|
|
} else {
|
|
const sectionID = contentCard.closest('.article-section').dataset.sectionId
|
|
const content = DOMPurify.sanitize(contentCard.querySelector('.content__area').innerHTML.split('\n').map(v => v.trim()).filter(v => v).join('\n'), { FORBID_ATTR: ['style'], ADD_ATTR: ['target'] })
|
|
.replace(/b>/gi, 'strong>').replace(/i>/gi, 'em>')
|
|
const contributors = Object.keys(getIterationDetails(contentID).contributors)
|
|
selectedContent.set(contentID, { content, sectionID, contributors })
|
|
}
|
|
const animOptions = {
|
|
duration: 150,
|
|
easing: 'ease',
|
|
}
|
|
const selectedContentSize = selectedContent.size
|
|
getRef('selected_entries_no').textContent = selectedContentSize
|
|
if (selectedContentSize === 1 && !isContentSelected) {
|
|
getRef('article_name_wrapper').classList.remove('hide-completely')
|
|
getRef('selected_content_options').classList.add('hide-completely')
|
|
animateTo(getRef('article_name_wrapper'), slideOutLeft, animOptions)
|
|
.onfinish = () => {
|
|
isContentSelected = true
|
|
getRef('article_name_wrapper').classList.add('hide-completely')
|
|
getRef('selected_content_options').classList.remove('hide-completely')
|
|
animateTo(getRef('selected_content_options'), slideInLeft, animOptions)
|
|
}
|
|
} else if (selectedContent.size === 0) {
|
|
getRef('article_name_wrapper').classList.add('hide-completely')
|
|
getRef('selected_content_options').classList.remove('hide-completely')
|
|
animateTo(getRef('selected_content_options'), slideOutRight, animOptions)
|
|
.onfinish = () => {
|
|
isContentSelected = false
|
|
getRef('article_name_wrapper').classList.remove('hide-completely')
|
|
getRef('selected_content_options').classList.add('hide-completely')
|
|
animateTo(getRef('article_name_wrapper'), slideInRight, animOptions)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
function formatSelectedContent() {
|
|
const composedDocumentStructure = {}
|
|
floGlobals.appObjects[floGlobals.currentArticle.id].sections.forEach(section => composedDocumentStructure[section.id] = [])
|
|
selectedContent.forEach(({ content, sectionID }, uid) => {
|
|
composedDocumentStructure[sectionID].push({ content, uid })
|
|
})
|
|
for (const section in composedDocumentStructure) {
|
|
if (!composedDocumentStructure[section].length)
|
|
delete composedDocumentStructure[section]
|
|
}
|
|
return composedDocumentStructure
|
|
}
|
|
|
|
function selectAll() {
|
|
getRef('article_wrapper').querySelectorAll('sm-checkbox').forEach(checkbox => checkbox.checked = true)
|
|
}
|
|
|
|
function clearSelection() {
|
|
selectedContent.forEach((v, key) => {
|
|
getRef('article_wrapper').querySelector(`.content-card[data-uid="${key}"] sm-checkbox`).checked = false
|
|
})
|
|
selectedContent.clear()
|
|
}
|
|
|
|
|
|
let currentOptionsPanel = ''
|
|
function toggleOptionsPanel(type) {
|
|
const animInOptions = {
|
|
duration: 200,
|
|
fill: 'forwards',
|
|
easing: 'ease'
|
|
}
|
|
const animOutOptions = {
|
|
duration: 100,
|
|
fill: 'forwards',
|
|
easing: 'ease'
|
|
}
|
|
|
|
if (getRef('options_panel').classList.contains('hide-completely')) {
|
|
getRef('options_panel').classList.remove('hide-completely')
|
|
getRef('options_panel').animate(slideInDown, animInOptions)
|
|
getRef('article_wrapper').animate([
|
|
{
|
|
transform: 'translateY(-3rem)'
|
|
}, {
|
|
transform: 'translateY(0)'
|
|
}
|
|
], animInOptions)
|
|
} else if (currentOptionsPanel === type) {
|
|
getRef('options_panel').animate(slideOutUp, animOutOptions)
|
|
.onfinish = () => {
|
|
getRef('options_panel').classList.add('hide-completely')
|
|
}
|
|
getRef('article_wrapper').animate([
|
|
{
|
|
transform: 'translateY(0)'
|
|
}, {
|
|
transform: 'translateY(-3rem)'
|
|
}
|
|
], animOutOptions).onfinish = e => e.target.cancel()
|
|
}
|
|
currentOptionsPanel = type
|
|
}
|
|
|
|
function toggleFilterPanel() {
|
|
getRef('filter_panel').classList.remove('hide-completely')
|
|
getRef('article_outline_panel').classList.add('hide-completely')
|
|
toggleOptionsPanel('filter')
|
|
}
|
|
function toggleOutlinePanel() {
|
|
getRef('filter_panel').classList.add('hide-completely')
|
|
getRef('article_outline_panel').classList.remove('hide-completely')
|
|
toggleOptionsPanel('outline')
|
|
}
|
|
getRef('article_outline').addEventListener('click', e => {
|
|
if (e.target.closest('.outline-button')) {
|
|
const sectionID = e.target.closest('.outline-button').dataset.sectionId
|
|
const target = document.querySelector(`.heading[data-section-id="${sectionID}"]`)
|
|
getRef('article_wrapper').scrollTo({
|
|
behavior: 'smooth',
|
|
top: target.getBoundingClientRect().top - getRef('article_wrapper').getBoundingClientRect().top + getRef('article_wrapper').scrollTop
|
|
})
|
|
}
|
|
})
|
|
function transformScroll(event) {
|
|
if (!event.deltaY) {
|
|
return;
|
|
}
|
|
|
|
// event.currentTarget.scrollLeft += event.deltaY + event.deltaX;
|
|
event.currentTarget.scrollBy({ left: event.deltaY * 5, behavior: 'smooth' });
|
|
event.preventDefault();
|
|
}
|
|
|
|
getRef('options_panel').addEventListener('wheel', transformScroll);
|
|
const activeHeading = debounce((target) => {
|
|
[...getRef('article_outline').children].forEach(elem => elem.classList.remove('outline-button--active'))
|
|
target.classList.add('outline-button--active')
|
|
target.scrollIntoView({ behavior: "smooth", block: "end", inline: "center" });
|
|
getRef('options_panel').scrollTo({
|
|
behavior: 'smooth',
|
|
left: target.getBoundingClientRect().left - getRef('options_panel').getBoundingClientRect().left + getRef('options_panel').scrollLeft - ((getRef('options_panel').getBoundingClientRect().width / 2) - target.getBoundingClientRect().width / 2)
|
|
})
|
|
}, 150)
|
|
|
|
const headingIntersectionObserver = new IntersectionObserver(entries => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
activeHeading(getRef('article_outline').querySelector(`[data-section-id="${entry.target.dataset.sectionId}"]`))
|
|
}
|
|
})
|
|
}, {
|
|
threshold: 0.6
|
|
})
|
|
|
|
function sortSectionEntries() {
|
|
const sortByScore = getRef('sort_content_list').value === 'score'
|
|
const originalObj = {}
|
|
for (const sectionID in floGlobals.currentArticle.sections) {
|
|
originalObj[sectionID] = [...floGlobals.currentArticle.sections[sectionID].uniqueEntries]
|
|
floGlobals.currentArticle.sections[sectionID].uniqueEntries.sort((a, b) => {
|
|
const arrayA = floGlobals.currentArticle.uniqueEntries[a].iterations
|
|
const arrayB = floGlobals.currentArticle.uniqueEntries[b].iterations
|
|
return getGenData(arrayB[arrayB.length - 1])[sortByScore ? 'score' : 'timestamp'] - getGenData(arrayA[arrayA.length - 1])[sortByScore ? 'score' : 'timestamp']
|
|
})
|
|
}
|
|
for (const sectionID in floGlobals.currentArticle.sections) {
|
|
if (floGlobals.currentArticle.sections[sectionID].uniqueEntries.some((v, index) => v !== originalObj[sectionID][index])) {
|
|
const allContentCards = {}
|
|
const section = getRef('article_wrapper').querySelector(`.article-section[data-section-id="${sectionID}"]`)
|
|
section.querySelectorAll('.content-card').forEach(card => {
|
|
allContentCards[card.dataset.uid] = card.cloneNode(true)
|
|
})
|
|
let emptyCard
|
|
if (!floGlobals.isSubAdmin)
|
|
emptyCard = section.firstElementChild.cloneNode(true)
|
|
section.innerHTML = ''
|
|
const frag = document.createDocumentFragment()
|
|
let toRender
|
|
if (floGlobals.currentArticle.sections[sectionID].expanded)
|
|
toRender = floGlobals.currentArticle.sections[sectionID].uniqueEntries
|
|
else
|
|
toRender = floGlobals.currentArticle.sections[sectionID].uniqueEntries.slice(0, maxCardsPerSection)
|
|
toRender.forEach((entry, index) => {
|
|
frag.append(allContentCards[entry] || render.contentCard(entry))
|
|
})
|
|
if (!floGlobals.isSubAdmin)
|
|
section.append(emptyCard)
|
|
section.append(frag)
|
|
}
|
|
}
|
|
}
|
|
getRef('sort_content_list').addEventListener('change', sortSectionEntries)
|
|
function renderSectionList() {
|
|
if (!floGlobals.isSubAdmin) return
|
|
getRef('edit_article_title').value = floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title
|
|
const frag = document.createDocumentFragment()
|
|
floGlobals.appObjects[floGlobals.currentArticle.id].sections.forEach(section => {
|
|
frag.append(render.sectionCard(section))
|
|
})
|
|
getRef('section_list_container').innerHTML = ''
|
|
getRef('section_list_container').append(frag)
|
|
}
|
|
function insertEmptySection() {
|
|
const emptySection = html.node`
|
|
<div class="section-card flex align-center">
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M9,18h12v-2H9V18z M3,6v2h18V6H3z M9,13h12v-2H9V13z"/></g></svg>
|
|
<input placeholder="New section title"/>
|
|
<button class="remove">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none"></path>
|
|
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
`
|
|
getRef('section_list_container').append(emptySection)
|
|
emptySection.querySelector('input').focus()
|
|
}
|
|
getRef('section_list_container').addEventListener('click', e => {
|
|
if (e.target.closest('.remove')) {
|
|
e.target.closest('.section-card').remove()
|
|
}
|
|
})
|
|
|
|
function saveSectionEdit() {
|
|
if (floGlobals.isSubAdmin) {
|
|
const newArticleTitle = getRef('edit_article_title').value.trim()
|
|
if (newArticleTitle !== '') {
|
|
if (floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title !== newArticleTitle) {
|
|
floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title = newArticleTitle
|
|
floCloudAPI.updateObjectData('cc')
|
|
.then((res) => {
|
|
getRef('current_article_title').textContent = newArticleTitle
|
|
notify('Renamed article', 'success')
|
|
}).catch(err => {
|
|
notify(err, 'error')
|
|
})
|
|
|
|
}
|
|
} else {
|
|
getRef('edit_article_title').value = floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title
|
|
}
|
|
const changedSections = [];
|
|
[...getRef('section_list_container').children].forEach(section => {
|
|
const title = section.querySelector('input').value.trim()
|
|
const sectionID = section.dataset.sectionId
|
|
if (title !== '')
|
|
changedSections.push({
|
|
id: sectionID || floCrypto.randString(16, true),
|
|
title
|
|
})
|
|
})
|
|
const didAddSection = changedSections.length > floGlobals.appObjects[floGlobals.currentArticle.id].sections.length
|
|
let didTitlesChange = false
|
|
if (!didAddSection) {
|
|
didTitlesChange = floGlobals.appObjects[floGlobals.currentArticle.id].sections.some(({ title }, index) => title !== changedSections[index].title)
|
|
}
|
|
if (didAddSection || didTitlesChange) {
|
|
floGlobals.appObjects[floGlobals.currentArticle.id].sections = changedSections
|
|
floCloudAPI.updateObjectData(floGlobals.currentArticle.id)
|
|
.then((res) => {
|
|
render.sectionOutline()
|
|
const frag = document.createDocumentFragment();
|
|
const currentSections = {}
|
|
getRef('article_wrapper').querySelectorAll('.heading').forEach(heading => {
|
|
currentSections[heading.dataset.sectionId] = {
|
|
title: heading.textContent,
|
|
ref: heading
|
|
}
|
|
})
|
|
changedSections.forEach(elem => {
|
|
const { title, id } = elem
|
|
if (currentSections.hasOwnProperty(id)) {
|
|
currentSections[id].ref.textContent = title
|
|
} else {
|
|
floGlobals.currentArticle.sections[id] = { title, uniqueEntries: [] }
|
|
frag.append(render.section(id, floGlobals.currentArticle.sections[id]))
|
|
}
|
|
})
|
|
getRef('article_wrapper').append(frag)
|
|
notify('Sections updated', 'success')
|
|
closePopup()
|
|
})
|
|
.catch(err => {
|
|
notify(err, 'error')
|
|
})
|
|
}
|
|
} else {
|
|
closePopup()
|
|
notify('This action requires sub-admin privileges', 'error')
|
|
}
|
|
}
|
|
|
|
|
|
function getArticleList() {
|
|
const articleList = floGlobals.appObjects.cc.articleList
|
|
const sortBy = getRef('sort_article_list').value
|
|
const arrayOfObject = []
|
|
for (const key in articleList) {
|
|
const { timestamp, title } = articleList[key]
|
|
arrayOfObject.push({
|
|
uid: key,
|
|
timestamp,
|
|
title
|
|
})
|
|
}
|
|
if (sortBy === 'az') {
|
|
arrayOfObject.sort((a, b) => a.title.localeCompare(b.title, 'en', { numeric: true, sensitivity: 'base' }))
|
|
} else {
|
|
arrayOfObject.sort((a, b) => b.timestamp - a.timestamp)
|
|
}
|
|
return arrayOfObject
|
|
}
|
|
|
|
getRef('article_list_search').addEventListener('input', e => {
|
|
const searchKey = e.target.value.trim()
|
|
const options = {
|
|
keys: ['title'],
|
|
threshold: 0.3
|
|
}
|
|
const fuse = new Fuse(getArticleList(), options)
|
|
renderArticleList(searchKey === '' ? undefined : fuse.search(searchKey).map(v => v.item))
|
|
})
|
|
getRef('sort_article_list').addEventListener('change', e => { renderArticleList() })
|
|
|
|
function renderArticleList(articleList) {
|
|
const defaultArticle = floGlobals.appObjects.cc.defaultArticle;
|
|
const articlesList = (articleList || getArticleList()).map((article) => {
|
|
const isDefaultArticle = defaultArticle === article.uid
|
|
return render.articleLink(article, isDefaultArticle, getRef('article_list'))
|
|
})
|
|
renderElem(getRef('article_list'), html`${articlesList}`)
|
|
}
|
|
|
|
|
|
function getIterationDetails(uid, targetIndex) {
|
|
const contributors = {}
|
|
const limit = targetIndex || floGlobals.currentArticle.uniqueEntries[uid].iterations.length - 1
|
|
for (let i = 0; i <= limit; i++) {
|
|
const { editor, timestamp } = getGenData(floGlobals.currentArticle.uniqueEntries[uid].iterations[i])
|
|
contributors[editor] = timestamp
|
|
}
|
|
const { html, hash, score } = getGenData(floGlobals.currentArticle.uniqueEntries[uid].iterations[limit]);
|
|
return {
|
|
html,
|
|
contributors,
|
|
hash,
|
|
score,
|
|
vectorClock: floGlobals.currentArticle.uniqueEntries[uid].iterations[limit]
|
|
}
|
|
}
|
|
floGlobals.versionHistory = {
|
|
isOpen: false,
|
|
entryUid: ''
|
|
}
|
|
function showVersionHistory(uid) {
|
|
floGlobals.versionHistory.entryUid = uid
|
|
const { iterations } = floGlobals.currentArticle.uniqueEntries[uid]
|
|
const frag = document.createDocumentFragment()
|
|
iterations.forEach((vectorClock, index) => {
|
|
const oldText = index !== 0 ? getGenData(iterations[index - 1]).plainText : ''
|
|
frag.prepend(render.historyEntry(getGenData(vectorClock), oldText))
|
|
})
|
|
getRef('version_timeline').innerHTML = ''
|
|
getRef('version_timeline').append(frag)
|
|
if (!floGlobals.versionHistory.isOpen) {
|
|
getRef('version_history_panel').classList.remove('hide-completely')
|
|
getRef('main_page').classList.add('active-sidebar')
|
|
floGlobals.versionHistory.isOpen = true
|
|
}
|
|
}
|
|
|
|
function hideVersionHistory() {
|
|
if (floGlobals.versionHistory.isOpen) {
|
|
getRef('version_history_panel').classList.add('hide-completely')
|
|
getRef('version_timeline').innerHTML = ''
|
|
getRef('main_page').classList.remove('active-sidebar')
|
|
floGlobals.versionHistory.isOpen = false
|
|
}
|
|
}
|
|
|
|
function normalizeText(target) {
|
|
if (target) {
|
|
for (const child of target.children) {
|
|
if (child.textContent === ' ') {
|
|
child.after(document.createTextNode(' '))
|
|
child.remove()
|
|
}
|
|
else if (child.textContent.trim() === '')
|
|
child.remove()
|
|
}
|
|
target.normalize()
|
|
}
|
|
}
|
|
|
|
function manageFormattingOptions() {
|
|
const selection = window.getSelection();
|
|
if (selection.isCollapsed) {
|
|
getRef('text_toolbar').classList.add('hide-completely')
|
|
document.querySelectorAll('.formatting-button').forEach(elem => elem.classList.remove('active'))
|
|
} else {
|
|
getRef('create_link_href').value = ''
|
|
if (isMobileView) {
|
|
getRef('text_toolbar').classList.remove('hide-completely')
|
|
getRef('text_toolbar').style.transform = `none`
|
|
} else {
|
|
const pos = selection.getRangeAt(0).getBoundingClientRect()
|
|
const leftOffset = pos.left + 200 < window.innerWidth ? pos.left : pos.left - 220 + window.innerWidth - pos.left
|
|
const topOffeset = (pos.bottom + window.pageYOffset + 64) > window.innerHeight ? pos.top + window.pageYOffset - 56 : pos.bottom + window.pageYOffset
|
|
getRef('text_toolbar').style.transform = `translate(${leftOffset}px, calc(${topOffeset}px + 1rem))`
|
|
getRef('text_toolbar').classList.remove('hide-completely')
|
|
getRef('text_toolbar').style.transform = `translate(${leftOffset}px, calc(${topOffeset}px + 0.3rem))`
|
|
}
|
|
getRef('formatting_options').classList.remove('hide-completely')
|
|
getRef('link_panel').classList.add('hide-completely')
|
|
}
|
|
}
|
|
|
|
const detectFormatting = debounce((e) => {
|
|
manageFormattingOptions()
|
|
if (!window.getSelection().isCollapsed) {
|
|
[
|
|
['bold', 'strong_button'],
|
|
['italic', 'em_button'],
|
|
['underline', 'u_button'],
|
|
['superscript', 'sup_button'],
|
|
['subscript', 'sub_button'],
|
|
['link', 'a_button'],
|
|
].forEach(([state, id]) => {
|
|
if (state === 'link' ? isLink() : document.queryCommandState(state)) {
|
|
getRef(id).classList.add('active')
|
|
} else {
|
|
getRef(id).classList.remove('active')
|
|
}
|
|
})
|
|
}
|
|
}, 300)
|
|
|
|
function saveSelection() {
|
|
if (window.getSelection) {
|
|
const sel = window.getSelection();
|
|
if (sel.getRangeAt && sel.rangeCount) {
|
|
const ranges = [];
|
|
for (let i = 0, len = sel.rangeCount; i < len; ++i) {
|
|
ranges.push(sel.getRangeAt(i));
|
|
}
|
|
return ranges;
|
|
}
|
|
} else if (document.selection && document.selection.createRange) {
|
|
return document.selection.createRange();
|
|
}
|
|
return null;
|
|
}
|
|
function restoreSelection(savedSel) {
|
|
if (savedSel) {
|
|
if (window.getSelection) {
|
|
const sel = window.getSelection();
|
|
sel.removeAllRanges();
|
|
for (let i = 0, len = savedSel.length; i < len; ++i) {
|
|
sel.addRange(savedSel[i]);
|
|
}
|
|
} else if (document.selection && savedSel.select) {
|
|
savedSel.select();
|
|
}
|
|
currentSelection = null
|
|
}
|
|
}
|
|
|
|
let currentSelection
|
|
function toggleLinkPanel(elem) {
|
|
if (elem && elem.classList.contains('active')) {
|
|
const selection = window.getSelection()
|
|
const startA = selection.anchorNode.parentNode
|
|
const endA = selection.focusNode.parentNode
|
|
getRef('create_link_href').value = (startA || endA).getAttribute('href')
|
|
} else {
|
|
getRef('create_link_href').value = ''
|
|
}
|
|
if (getRef('link_panel').classList.contains('hide-completely')) {
|
|
currentSelection = saveSelection()
|
|
} else {
|
|
|
|
}
|
|
getRef('formatting_options').classList.toggle('hide-completely')
|
|
getRef('link_panel').classList.toggle('hide-completely')
|
|
}
|
|
|
|
function isLink() {
|
|
const selection = window.getSelection()
|
|
const startA = selection.anchorNode.parentNode.tagName === 'A'
|
|
const endA = selection.focusNode.parentNode.tagName === 'A'
|
|
return startA || endA
|
|
}
|
|
|
|
function createLink() {
|
|
restoreSelection(currentSelection)
|
|
const url = getRef('create_link_href').value
|
|
if (isLink()) {
|
|
const selection = window.getSelection()
|
|
const startA = selection.anchorNode.parentNode
|
|
const endA = selection.focusNode.parentNode;
|
|
(startA || endA).setAttribute('href', url)
|
|
} else {
|
|
replaceSelectedText(createElement('a', {
|
|
attributes: {
|
|
href: url,
|
|
target: "_blank",
|
|
rel: "noopener noreferrer"
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
|
|
function replaceSelectedText(node) {
|
|
const selection = window.getSelection();
|
|
if (!selection.isCollapsed && selection.rangeCount) {
|
|
const range = selection.getRangeAt(0);
|
|
const originalText = range.toString()
|
|
range.deleteContents()
|
|
node.textContent = originalText
|
|
range.insertNode(node);
|
|
selection.anchorNode.parentNode.closest('.content__area')?.focus()
|
|
}
|
|
}
|
|
|
|
function insertNode(node) {
|
|
const selection = window.getSelection();
|
|
if (selection.rangeCount) {
|
|
const range = selection.getRangeAt(0);
|
|
range.insertNode(node)
|
|
}
|
|
}
|
|
|
|
function getSignedIn() {
|
|
return new Promise((resolve, reject) => {
|
|
if (window.location.hash.includes('sign_in') || window.location.hash.includes('sign_up')) {
|
|
showPage(window.location.hash)
|
|
} else {
|
|
showPage('landing', { isPreview: location.hash.includes('preview') })
|
|
}
|
|
getRef('sign_in_button').onclick = () => {
|
|
resolve(getRef('private_key_field').value.trim())
|
|
getRef('private_key_field').value = ''
|
|
showPage('loading')
|
|
}
|
|
getRef('sign_up_button').onclick = () => {
|
|
resolve(getRef('generated_private_key').value.trim())
|
|
getRef('generated_private_key').value = ''
|
|
showPage('loading')
|
|
}
|
|
})
|
|
}
|
|
function signOut() {
|
|
getConfirmation('Sign out?', 'You are about to sign out of the app, continue?', 'Stay', 'Leave')
|
|
.then(async (res) => {
|
|
if (res) {
|
|
await floDapps.clearCredentials()
|
|
location.reload()
|
|
}
|
|
})
|
|
}
|
|
</script>
|
|
<script id="onLoadStartUp">
|
|
let maxCardsPerSection
|
|
|
|
function onLoadStartUp() {
|
|
|
|
//floDapps.addStartUpFunction('Sample', Promised Function)
|
|
//floDapps.setAppObjectStores({sampleObs1:{}, sampleObs2:{options{autoIncrement:true, keyPath:'SampleKey'}, Indexes:{sampleIndex:{}}}})
|
|
floDapps.setCustomPrivKeyInput(getSignedIn)
|
|
|
|
floDapps.launchStartUp().then(async result => {
|
|
getRef('user_flo_id').value = myFloID
|
|
document.querySelectorAll('.user-id').forEach(elem => {
|
|
elem.textContent = myFloID
|
|
})
|
|
floGlobals.isSubAdmin = floGlobals.subAdmins.includes(myFloID)
|
|
maxCardsPerSection = floGlobals.isSubAdmin ? 3 : 2
|
|
if (floGlobals.isSubAdmin) {
|
|
document.querySelectorAll('.admin-option').forEach(elem => elem.classList.remove('hide-completely'))
|
|
} else {
|
|
document.querySelectorAll('.admin-option').forEach(elem => elem.classList.add('hide-completely'))
|
|
}
|
|
await Promise.all([
|
|
floCloudAPI.requestObjectData('cc'),
|
|
])
|
|
getRef('preview_options').innerHTML = `
|
|
${floGlobals.isSubAdmin ? `
|
|
<button class="icon-only" title="Share preview" onclick="sharePreview()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24" /> </g> <g> <path d="M16,5l-1.42,1.42l-1.59-1.59V16h-1.98V4.83L9.42,6.42L8,5l4-4L16,5z M20,10v11c0,1.1-0.9,2-2,2H6c-1.11,0-2-0.9-2-2V10 c0-1.11,0.89-2,2-2h3v2H6v11h12V10h-3V8h3C19.1,8,20,8.89,20,10z" /> </g> </svg>
|
|
</button>
|
|
<button class="icon-only" title="Export selection" onclick="exportSelection()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24" /> </g> <g> <path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z" /> </g> </svg>
|
|
</button>
|
|
`: ''}
|
|
<sm-button variant="primary" onclick="${floGlobals.isSubAdmin ? 'publishArticle()' : 'exportSelection()'}">
|
|
${floGlobals.isSubAdmin ? `
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0V0z" fill="none" /> <path d="M5 4h14v2H5zm0 10h4v6h6v-6h4l-7-7-7 7zm8-2v6h-2v-6H9.83L12 9.83 14.17 12H13z" /> </svg>
|
|
Publish
|
|
`: `
|
|
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24" /> </g> <g> <path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z" /> </g> </svg>
|
|
Download
|
|
`}
|
|
</sm-button>
|
|
`
|
|
if (window.location.hash.includes('sign_in') || window.location.hash.includes('sign_up')) {
|
|
const hash = floGlobals.preview ? floGlobals.preview : ' '
|
|
history.replaceState(null, null, hash)
|
|
}
|
|
showPage(window.location.hash, { firstLoad: true })
|
|
console.log(result)
|
|
}).catch(error => console.error(error))
|
|
}
|
|
</script>
|
|
<script id="lib" src="scripts/lib.js"></script>
|
|
<script id="floCrypto" src="scripts/floCrypto.js"></script>
|
|
<script id="floBlockchainAPI" src="scripts/floBlockchainAPI.js"></script>
|
|
<script id="compactIDB" src="scripts/compactIDB.js"></script>
|
|
<script id="floCloudAPI" src="scripts/floCloudAPI.js"></script>
|
|
<script id="floDapps" src="scripts/floDapps.js"></script>
|
|
<template id="head_template">
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
<title></title>
|
|
<style>
|
|
* {
|
|
padding: 0;
|
|
margin: 0;
|
|
-webkit-box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
font-family: "Roboto", sans-serif
|
|
}
|
|
|
|
:root {
|
|
font-size: clamp(1rem, 1.2vmax, 1.2rem)
|
|
}
|
|
|
|
html,
|
|
body {
|
|
height: 100%;
|
|
scroll-behavior: smooth
|
|
}
|
|
|
|
body {
|
|
color: rgba(var(--text-color), 1);
|
|
background: rgba(var(--background-color), 1)
|
|
}
|
|
|
|
body,
|
|
body * {
|
|
--accent-color: rgb(0, 156, 78);
|
|
--text-color: 36, 36, 36;
|
|
--background-color: 252, 252, 252;
|
|
--foreground-color: rgb(255, 255, 255);
|
|
--danger-color: rgb(255, 75, 75);
|
|
--like-color: #e91e63;
|
|
scrollbar-width: thin
|
|
}
|
|
|
|
body[data-theme=dark],
|
|
body[data-theme=dark] * {
|
|
--accent-color: rgb(14, 230, 122);
|
|
--text-color: 230, 230, 230;
|
|
--text-color-light: 170, 170, 170;
|
|
--background-color: 10, 10, 10;
|
|
--foreground-color: rgb(24, 24, 24);
|
|
--danger-color: rgb(255, 106, 106)
|
|
}
|
|
|
|
body[data-theme=dark] sm-popup::part(popup) {
|
|
background-color: var(--foreground-color)
|
|
}
|
|
|
|
p {
|
|
font-size: .9rem;
|
|
max-width: 70ch;
|
|
color: rgba(var(--text-color), 0.8)
|
|
}
|
|
|
|
p * {
|
|
font-family: inherit
|
|
}
|
|
|
|
a {
|
|
text-decoration: none;
|
|
color: var(--accent-color);
|
|
}
|
|
|
|
h1,
|
|
h2,
|
|
h3,
|
|
h4,
|
|
h5,
|
|
h6 {
|
|
font-weight: 400;
|
|
font-family: "Calistoga", cursive;
|
|
}
|
|
|
|
sm-copy {
|
|
font-size: 0.9rem;
|
|
--button-border-radius: 0.5rem;
|
|
}
|
|
|
|
.flex {
|
|
display: -webkit-box;
|
|
display: -ms-flexbox;
|
|
display: flex
|
|
}
|
|
|
|
.direction-column {
|
|
-webkit-box-orient: vertical;
|
|
-webkit-box-direction: normal;
|
|
-ms-flex-direction: column;
|
|
flex-direction: column
|
|
}
|
|
|
|
.grid {
|
|
display: grid
|
|
}
|
|
|
|
.flow-column {
|
|
grid-auto-flow: column
|
|
}
|
|
|
|
.gap-0-5 {
|
|
gap: .5rem
|
|
}
|
|
|
|
.gap-1 {
|
|
gap: 1rem
|
|
}
|
|
|
|
.gap-1-5 {
|
|
gap: 1.5rem
|
|
}
|
|
|
|
.gap-2 {
|
|
gap: 2rem
|
|
}
|
|
|
|
.gap-3 {
|
|
gap: 3rem
|
|
}
|
|
|
|
.justify-self-center {
|
|
justify-self: center
|
|
}
|
|
|
|
.justify-self-start {
|
|
justify-self: start
|
|
}
|
|
|
|
.justify-self-end {
|
|
justify-self: end
|
|
}
|
|
|
|
.align-center {
|
|
align-items: center;
|
|
}
|
|
|
|
.icon {
|
|
width: 1.2rem;
|
|
height: 1.2rem;
|
|
fill: rgba(var(--text-color), 0.8);
|
|
-ms-flex-negative: 0;
|
|
flex-shrink: 0
|
|
}
|
|
|
|
button,
|
|
.button {
|
|
-webkit-user-select: none;
|
|
-moz-user-select: none;
|
|
-ms-user-select: none;
|
|
user-select: none;
|
|
position: relative;
|
|
display: -webkit-inline-box;
|
|
display: -ms-inline-flexbox;
|
|
display: inline-flex;
|
|
border: none;
|
|
background-color: transparent;
|
|
overflow: hidden;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
-webkit-transition: -webkit-transform .3s;
|
|
transition: -webkit-transform .3s;
|
|
transition: transform .3s;
|
|
transition: transform .3s, -webkit-transform .3s;
|
|
-webkit-tap-highlight-color: transparent;
|
|
-webkit-box-align: center;
|
|
-ms-flex-align: center;
|
|
align-items: center;
|
|
font-size: .9rem;
|
|
font-weight: 500
|
|
}
|
|
|
|
.button {
|
|
white-space: nowrap;
|
|
padding: .6rem 1rem;
|
|
border-radius: .3rem;
|
|
background-color: rgba(var(--text-color), 0.06);
|
|
color: rgba(var(--text-color), 0.8);
|
|
-webkit-box-pack: center;
|
|
-ms-flex-pack: center;
|
|
justify-content: center
|
|
}
|
|
|
|
.button--primary {
|
|
background-color: var(--accent-color);
|
|
color: rgba(var(--background-color), 1)
|
|
}
|
|
|
|
button:active,
|
|
.button:active,
|
|
sm-button:not([disabled]):active,
|
|
.interact:active {
|
|
-webkit-transform: scale(0.9);
|
|
transform: scale(0.9)
|
|
}
|
|
|
|
#confirmation_popup,
|
|
#prompt_popup {
|
|
-webkit-box-orient: vertical;
|
|
-webkit-box-direction: normal;
|
|
-ms-flex-direction: column;
|
|
flex-direction: column;
|
|
}
|
|
|
|
#confirmation_popup h4 {
|
|
font-weight: 500;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
#confirmation_popup sm-button {
|
|
margin: 0;
|
|
}
|
|
|
|
#confirmation_popup .flex {
|
|
padding: 0;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
#confirmation_popup .flex sm-button:first-of-type {
|
|
margin-right: 0.6rem;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.popup__header {
|
|
display: grid;
|
|
gap: .5rem;
|
|
width: 100%;
|
|
padding: 0 1.5rem 0 .5rem;
|
|
-webkit-box-align: center;
|
|
-ms-flex-align: center;
|
|
align-items: center;
|
|
grid-template-columns: auto 1fr auto
|
|
}
|
|
|
|
.popup__header__close {
|
|
padding: .5rem
|
|
}
|
|
|
|
#sign_in,
|
|
#sign_up {
|
|
-webkit-box-align: center;
|
|
-ms-flex-align: center;
|
|
align-items: center
|
|
}
|
|
|
|
#sign_in sm-form,
|
|
#sign_up sm-form {
|
|
margin: 2rem 0;
|
|
--gap: 1rem
|
|
}
|
|
|
|
#sign_in header,
|
|
#sign_up header {
|
|
padding: 1.5rem 0
|
|
}
|
|
|
|
#sign_up sm-copy {
|
|
font-size: .9rem
|
|
}
|
|
|
|
#sign_up h5 {
|
|
font-weight: 500;
|
|
color: rgba(var(--text-color), 0.8)
|
|
}
|
|
|
|
.card {
|
|
padding: 1rem;
|
|
border-radius: .5rem;
|
|
background-color: rgba(var(--text-color), 0.04)
|
|
}
|
|
|
|
.warning {
|
|
background-color: khaki;
|
|
color: rgba(0, 0, 0, .7);
|
|
padding: 1rem;
|
|
border-radius: .5rem;
|
|
line-height: 1.5
|
|
}
|
|
|
|
#main_header {
|
|
display: grid;
|
|
gap: 1rem;
|
|
position: sticky;
|
|
top: 0;
|
|
padding: 1rem;
|
|
-webkit-box-align: center;
|
|
-ms-flex-align: center;
|
|
align-items: center;
|
|
grid-template-columns: 1fr auto auto;
|
|
background-color: rgba(var(--background-color), 1);
|
|
z-index: 1
|
|
}
|
|
|
|
.logo {
|
|
color: inherit;
|
|
display: grid;
|
|
-webkit-box-align: center;
|
|
-ms-flex-align: center;
|
|
align-items: center;
|
|
width: 100%;
|
|
grid-template-columns: auto 1fr;
|
|
gap: 0 .5rem;
|
|
margin-right: 1rem
|
|
}
|
|
|
|
.logo h4 {
|
|
text-transform: capitalize;
|
|
font-size: .9rem;
|
|
font-weight: 500
|
|
}
|
|
|
|
.main-logo {
|
|
height: 1.4rem;
|
|
width: 1.4rem;
|
|
fill: rgba(var(--text-color), 1);
|
|
stroke: none
|
|
}
|
|
|
|
article {
|
|
padding-bottom: 3rem;
|
|
gap: 1rem
|
|
}
|
|
|
|
#export_body {
|
|
position: relative;
|
|
margin-bottom: 1rem;
|
|
padding-bottom: 1.5rem;
|
|
}
|
|
|
|
#export_body::after {
|
|
justify-self: center;
|
|
position: absolute;
|
|
bottom: 0;
|
|
content: "";
|
|
width: 3rem;
|
|
height: .3rem;
|
|
border-radius: .5rem;
|
|
background-color: rgba(var(--text-color), 0.5)
|
|
}
|
|
|
|
article p {
|
|
line-height: 1.8;
|
|
font-size: 1rem
|
|
}
|
|
|
|
.page-layout {
|
|
display: grid;
|
|
grid-template-columns: 1rem minmax(0, 1fr) 1rem
|
|
}
|
|
|
|
.page-layout>* {
|
|
grid-column: 2/3
|
|
}
|
|
|
|
.hero-section {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr);
|
|
margin-bottom: 1.5rem;
|
|
padding-top: 1.5rem
|
|
}
|
|
|
|
time,
|
|
#reading_time {
|
|
font-size: .8rem
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.4rem;
|
|
margin-bottom: 1rem
|
|
}
|
|
|
|
h3:not(:first-of-type) {
|
|
margin-top: 2rem
|
|
}
|
|
|
|
.full-bleed {
|
|
grid-column: 1/-1
|
|
}
|
|
|
|
.quote-template {
|
|
position: relative;
|
|
padding: 1rem;
|
|
margin: 1rem 0;
|
|
border-radius: .2rem;
|
|
border: solid thin rgba(var(--text-color), 0.3);
|
|
-webkit-box-shadow: .3rem .5rem 0 .1rem rgba(var(--text-color), 0.8);
|
|
box-shadow: .3rem .5rem 0 .1rem rgba(var(--text-color), 0.8);
|
|
overflow: hidden;
|
|
justify-self: center;
|
|
padding-left: 1.3rem
|
|
}
|
|
|
|
.quote-template figcaption {
|
|
margin-top: .5rem;
|
|
color: rgba(var(--text-color), 0.8);
|
|
font-size: .8rem;
|
|
margin-left: auto
|
|
}
|
|
|
|
#article_contributors {
|
|
flex-wrap: wrap;
|
|
gap: 0.3rem;
|
|
margin: 0.5rem 0 1rem 0;
|
|
}
|
|
|
|
.contributor {
|
|
font-size: 0.8rem;
|
|
background-color: rgba(var(--text-color), 0.06);
|
|
border-radius: 0.3rem;
|
|
padding: 0.3rem 0.5rem;
|
|
}
|
|
|
|
.upvote {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
position: relative;
|
|
padding: .8rem;
|
|
border-radius: 2rem;
|
|
background-color: var(--foreground-color);
|
|
-webkit-box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .1);
|
|
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .1);
|
|
border: solid rgba(var(--text-color), 0.2) thin
|
|
}
|
|
|
|
.upvote>* {
|
|
pointer-events: none
|
|
}
|
|
|
|
.upvote:active {
|
|
-webkit-transform: none;
|
|
transform: none
|
|
}
|
|
|
|
.upvote:active .icon {
|
|
-webkit-transform: scale(0.7);
|
|
transform: scale(0.7)
|
|
}
|
|
|
|
.upvote.liked {
|
|
background-color: var(--like-color);
|
|
color: #fff
|
|
}
|
|
|
|
.upvote.liked .icon {
|
|
fill: #fff
|
|
}
|
|
|
|
.expanding-heart,
|
|
.ring {
|
|
grid-area: 1/1
|
|
}
|
|
|
|
.ring {
|
|
border: .1rem solid var(--like-color);
|
|
border-radius: 50%;
|
|
height: .5rem;
|
|
width: .5rem;
|
|
justify-self: center
|
|
}
|
|
|
|
.upvote .icon {
|
|
grid-area: 1/1;
|
|
fill: var(--like-color);
|
|
height: 1.5rem;
|
|
width: 1.5rem;
|
|
-webkit-transition: -webkit-transform .2s;
|
|
transition: -webkit-transform .2s;
|
|
transition: transform .2s;
|
|
transition: transform .2s, -webkit-transform .2s
|
|
}
|
|
|
|
.temp-count,
|
|
#like_count {
|
|
grid-area: 1/2
|
|
}
|
|
|
|
.temp-count:not(:empty),
|
|
#like_count:not(:empty) {
|
|
margin-left: .4rem
|
|
}
|
|
|
|
footer {
|
|
padding: 3rem 1.5rem;
|
|
justify-items: center
|
|
}
|
|
|
|
@media screen and (min-width: 40rem) {
|
|
sm-popup {
|
|
--width: 24rem
|
|
}
|
|
|
|
.popup__header {
|
|
padding: 1rem 1.5rem 0 1rem
|
|
}
|
|
|
|
.page-layout {
|
|
grid-template-columns: 1fr 60ch 1fr
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2rem
|
|
}
|
|
}
|
|
|
|
@media(any-hover: hover) {
|
|
::-webkit-scrollbar {
|
|
width: .5rem;
|
|
height: .5rem
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: rgba(var(--text-color), 0.3);
|
|
border-radius: 1rem
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(var(--text-color), 0.5)
|
|
}
|
|
}
|
|
|
|
.hide-completely {
|
|
display: none
|
|
}
|
|
</style>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Calistoga&family=Roboto:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
|
|
rel="stylesheet">
|
|
</template>
|
|
<template id="body_template">
|
|
<header id="main_header">
|
|
<a href="https://ranchimall.github.io/rmtimes/" class="logo">
|
|
<svg class="main-logo" viewBox="0 0 27.25 32">
|
|
<title>RanchiMall</title>
|
|
<path
|
|
d="M27.14,30.86c-.74-2.48-3-4.36-8.25-6.94a20,20,0,0,1-4.2-2.49,6,6,0,0,1-1.25-1.67,4,4,0,0,1,0-2.26c.37-1.08.79-1.57,3.89-4.55a11.66,11.66,0,0,0,3.34-4.67,6.54,6.54,0,0,0,.05-2.82C20,3.6,18.58,2,16.16.49c-.89-.56-1.29-.64-1.3-.24a3,3,0,0,1-.3.72l-.3.55L13.42.94C13,.62,12.4.26,12.19.15c-.4-.2-.73-.18-.72.05a9.39,9.39,0,0,1-.61,1.33s-.14,0-.27-.13C8.76.09,8-.27,8,.23A11.73,11.73,0,0,1,6.76,2.6C4.81,5.87,2.83,7.49.77,7.49c-.89,0-.88,0-.61,1,.22.85.33.92,1.09.69A5.29,5.29,0,0,0,3,8.33c.23-.17.45-.29.49-.26a2,2,0,0,1,.22.63A1.31,1.31,0,0,0,4,9.34a5.62,5.62,0,0,0,2.27-.87L7,8l.13.55c.19.74.32.82,1,.65a7.06,7.06,0,0,0,3.46-2.47l.6-.71-.06.64c-.17,1.63-1.3,3.42-3.39,5.42L6.73,14c-3.21,3.06-3,5.59.6,8a46.77,46.77,0,0,0,4.6,2.41c.28.13,1,.52,1.59.87,3.31,2,4.95,3.92,4.95,5.93a2.49,2.49,0,0,0,.07.77h0c.09.09,0,.1.9-.14a2.61,2.61,0,0,0,.83-.32,3.69,3.69,0,0,0-.55-1.83A11.14,11.14,0,0,0,17,26.81a35.7,35.7,0,0,0-5.1-2.91C9.37,22.64,8.38,22,7.52,21.17a3.53,3.53,0,0,1-1.18-2.48c0-1.38.71-2.58,2.5-4.23,2.84-2.6,3.92-3.91,4.67-5.65a3.64,3.64,0,0,0,.42-2A3.37,3.37,0,0,0,13.61,5l-.32-.74.29-.48c.17-.27.37-.63.46-.8l.15-.3.44.64a5.92,5.92,0,0,1,1,2.81,5.86,5.86,0,0,1-.42,1.94c0,.12-.12.3-.15.4a9.49,9.49,0,0,1-.67,1.1,28,28,0,0,1-4,4.29C8.62,15.49,8.05,16.44,8,17.78a3.28,3.28,0,0,0,1.11,2.76c.95,1,2.07,1.74,5.25,3.32,3.64,1.82,5.22,2.9,6.41,4.38A4.78,4.78,0,0,1,21.94,31a3.21,3.21,0,0,0,.14.92,1.06,1.06,0,0,0,.43-.05l.83-.22.46-.12-.06-.46c-.21-1.53-1.62-3.25-3.94-4.8a37.57,37.57,0,0,0-5.22-2.82A13.36,13.36,0,0,1,11,21.19a3.36,3.36,0,0,1-.8-4.19c.41-.85.83-1.31,3.77-4.15,2.39-2.31,3.43-4.13,3.43-6a5.85,5.85,0,0,0-2.08-4.29c-.23-.21-.44-.43-.65-.65A2.5,2.5,0,0,1,15.27.69a10.6,10.6,0,0,1,2.91,2.78A4.16,4.16,0,0,1,19,6.16a4.91,4.91,0,0,1-.87,3c-.71,1.22-1.26,1.82-4.27,4.67a9.47,9.47,0,0,0-2.07,2.6,2.76,2.76,0,0,0-.33,1.54,2.76,2.76,0,0,0,.29,1.47c.57,1.21,2.23,2.55,4.65,3.73a32.41,32.41,0,0,1,5.82,3.24c2.16,1.6,3.2,3.16,3.2,4.8a1.94,1.94,0,0,0,.09.76,4.54,4.54,0,0,0,1.66-.4C27.29,31.42,27.29,31.37,27.14,30.86ZM6.1,7h0a3.77,3.77,0,0,1-1.46.45L4,7.51l.68-.83a25.09,25.09,0,0,0,3-4.82A12,12,0,0,1,8.28.76c.11-.12.77.32,1.53,1l.63.58-.57.84A10.34,10.34,0,0,1,6.1,7Zm5.71-1.78A9.77,9.77,0,0,1,9.24,7.18h0a5.25,5.25,0,0,1-1.17.28l-.58,0,.65-.78a21.29,21.29,0,0,0,2.1-3.12c.22-.41.42-.76.44-.79s.5.43.9,1.24L12,5ZM13.41,3a2.84,2.84,0,0,1-.45.64,11,11,0,0,1-.9-.91l-.84-.9.19-.45c.34-.79.39-.8,1-.31A9.4,9.4,0,0,1,13.8,2.33q-.18.34-.39.69Z" />
|
|
</svg>
|
|
<h4>RanchiMall Times</h4>
|
|
</a>
|
|
<theme-toggle></theme-toggle>
|
|
<button id="user_button" class="hide-completely" onclick="openPopup('user_popup')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M12 5.9c1.16 0 2.1.94 2.1 2.1s-.94 2.1-2.1 2.1S9.9 9.16 9.9 8s.94-2.1 2.1-2.1m0 9c2.97 0 6.1 1.46 6.1 2.1v1.1H5.9V17c0-.64 3.13-2.1 6.1-2.1M12 4C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 9c-2.67 0-8 1.34-8 4v3h16v-3c0-2.66-5.33-4-8-4z" />
|
|
</svg>
|
|
</button>
|
|
</header>
|
|
<article class="page-layout">
|
|
<section class="hero-section">
|
|
<h1 id="exported_title"></h1>
|
|
<div class="flex align-center">
|
|
<time id="exported_time"></time> • <span id="reading_time"></span>
|
|
</div>
|
|
</section>
|
|
<section id="export_body" class="grid gap-1"></section>
|
|
<section>
|
|
<h4>Article by -</h4>
|
|
<div id="article_contributors" class="flex"></div>
|
|
<span>created with RanchiMall Content collaboration app</span>
|
|
</section>
|
|
</article>
|
|
<script>
|
|
const themeToggle = document.createElement('template');
|
|
themeToggle.innerHTML = `
|
|
<style>
|
|
*{
|
|
padding: 0;
|
|
margin: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
:host{
|
|
cursor: pointer;
|
|
--height: 2.5rem;
|
|
--width: 2.5rem;
|
|
}
|
|
.theme-toggle {
|
|
display: flex;
|
|
position: relative;
|
|
width: 1.4rem;
|
|
height: 1.4rem;
|
|
cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
.theme-toggle::after{
|
|
content: '';
|
|
position: absolute;
|
|
height: var(--height);
|
|
width: var(--width);
|
|
top: 50%;
|
|
left: 50%;
|
|
opacity: 0;
|
|
border-radius: 50%;
|
|
pointer-events: none;
|
|
transition: transform 0.3s, opacity 0.3s;
|
|
transform: translate(-50%, -50%) scale(1.2);
|
|
background-color: rgba(var(--text-color), 0.12);
|
|
}
|
|
:host(:focus-within) .theme-toggle{
|
|
outline: none;
|
|
}
|
|
:host(:focus-within) .theme-toggle::after{
|
|
opacity: 1;
|
|
transform: translate(-50%, -50%) scale(1);
|
|
}
|
|
.icon {
|
|
position: absolute;
|
|
height: 100%;
|
|
width: 100%;
|
|
fill: rgba(var(--text-color), 1);
|
|
transition: transform 0.3s, opacity 0.1s;
|
|
}
|
|
|
|
.theme-switcher__checkbox {
|
|
display: none;
|
|
}
|
|
:host([checked]) .moon-icon {
|
|
transform: translateY(50%);
|
|
opacity: 0;
|
|
}
|
|
:host(:not([checked])) .sun-icon {
|
|
transform: translateY(50%);
|
|
opacity: 0;
|
|
}
|
|
</style>
|
|
<label class="theme-toggle" title="Change theme" tabindex="0">
|
|
<slot name="light-mode-icon">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon moon-icon" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><rect fill="none" height="24" width="24"/><path d="M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"/></svg>
|
|
</slot>
|
|
<slot name="dark-mode-icon">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon sun-icon" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><rect fill="none" height="24" width="24"/><path d="M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"/></svg>
|
|
</slot>
|
|
</label>
|
|
`;
|
|
|
|
class ThemeToggle extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
|
|
this.attachShadow({
|
|
mode: 'open'
|
|
}).append(themeToggle.content.cloneNode(true));
|
|
|
|
this.isChecked = false;
|
|
this.hasTheme = 'light';
|
|
|
|
this.toggleState = this.toggleState.bind(this);
|
|
this.fireEvent = this.fireEvent.bind(this);
|
|
this.handleThemeChange = this.handleThemeChange.bind(this);
|
|
}
|
|
static get observedAttributes() {
|
|
return ['checked'];
|
|
}
|
|
|
|
daylight() {
|
|
this.hasTheme = 'light';
|
|
document.body.dataset.theme = 'light';
|
|
this.setAttribute('aria-checked', 'false');
|
|
}
|
|
|
|
nightlight() {
|
|
this.hasTheme = 'dark';
|
|
document.body.dataset.theme = 'dark';
|
|
this.setAttribute('aria-checked', 'true');
|
|
}
|
|
|
|
toggleState() {
|
|
this.toggleAttribute('checked');
|
|
this.fireEvent();
|
|
}
|
|
handleKeyDown(e) {
|
|
if (e.key === ' ') {
|
|
this.toggleState();
|
|
}
|
|
}
|
|
handleThemeChange(e) {
|
|
if (e.detail.theme !== this.hasTheme) {
|
|
if (e.detail.theme === 'dark') {
|
|
this.setAttribute('checked', '');
|
|
}
|
|
else {
|
|
this.removeAttribute('checked');
|
|
}
|
|
}
|
|
}
|
|
|
|
fireEvent() {
|
|
this.dispatchEvent(
|
|
new CustomEvent('themechange', {
|
|
bubbles: true,
|
|
composed: true,
|
|
detail: {
|
|
theme: this.hasTheme
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.setAttribute('role', 'switch');
|
|
this.setAttribute('aria-label', 'theme toggle');
|
|
if (localStorage.getItem(`${window.location.hostname}-theme`) === "dark") {
|
|
this.nightlight();
|
|
this.setAttribute('checked', '');
|
|
} else if (localStorage.getItem(`${window.location.hostname}-theme`) === "light") {
|
|
this.daylight();
|
|
this.removeAttribute('checked');
|
|
}
|
|
else {
|
|
if (window.matchMedia(`(prefers-color-scheme: dark)`).matches) {
|
|
this.nightlight();
|
|
this.setAttribute('checked', '');
|
|
} else {
|
|
this.daylight();
|
|
this.removeAttribute('checked');
|
|
}
|
|
}
|
|
this.addEventListener("click", this.toggleState);
|
|
this.addEventListener("keydown", this.handleKeyDown);
|
|
document.addEventListener('themechange', this.handleThemeChange);
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
this.removeEventListener("click", this.toggleState);
|
|
this.removeEventListener("keydown", this.handleKeyDown);
|
|
document.removeEventListener('themechange', this.handleThemeChange);
|
|
}
|
|
|
|
attributeChangedCallback(name, oldVal, newVal) {
|
|
if (name === 'checked') {
|
|
if (this.hasAttribute('checked')) {
|
|
this.nightlight();
|
|
localStorage.setItem(`${window.location.hostname}-theme`, "dark");
|
|
} else {
|
|
this.daylight();
|
|
localStorage.setItem(`${window.location.hostname}-theme`, "light");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
window.customElements.define('theme-toggle', ThemeToggle);
|
|
</script>
|
|
</template>
|
|
<template id="voting_enabled">
|
|
<footer class="grid gap-1-5">
|
|
<h4>Loved the article? Don't forget leave a like.</h4>
|
|
<button id="upvote_button" class="button upvote">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0z" fill="none" />
|
|
<path
|
|
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
|
|
</svg>
|
|
<div id="like_count">Loading...</div>
|
|
</button>
|
|
</footer>
|
|
<sm-popup id="confirmation_popup">
|
|
<h4 id="confirm_title"></h4>
|
|
<p id="confirm_message"></p>
|
|
<div class="flex align-center">
|
|
<sm-button variant="no-outline" class="cancel-btn">Cancel</sm-button>
|
|
<sm-button variant="no-outline" class="submit-btn">OK</sm-button>
|
|
</div>
|
|
</sm-popup>
|
|
<sm-popup id="sign_in_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
</header>
|
|
<section id="sign_in" class="">
|
|
<div class="grid gap-0-5">
|
|
<h3>Sign in to like this article</h3>
|
|
<p>Liking an article supports the creators.</p>
|
|
</div>
|
|
<sm-form>
|
|
<sm-input id="private_key_field" type="password" placeholder="FLO private key"
|
|
error-text="Private key is invalid" data-private-key required></sm-input>
|
|
<sm-button id="sign_in_button" variant="primary" disabled>Sign In</sm-button>
|
|
</sm-form>
|
|
<div class="grid gap-0-5">
|
|
<p>New here? Generate your FLO credentials below to continue</p>
|
|
<button class="button" onclick="generateCredentials()">
|
|
Get FLO credentials
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0z" fill="none" />
|
|
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
<section id="sign_up" class="grid gap-1-5 hide-completely">
|
|
<button class="justify-self-start" onclick="goToSignIn()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
|
fill="#000000">
|
|
<path d="M0 0h24v24H0z" fill="none" />
|
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
|
|
</svg>
|
|
</button>
|
|
<div class="grid gap-0-5">
|
|
<h3>FLO credentials</h3>
|
|
<p>You can use FLO credentials with RanchiMall Times and all RanchiMall FLO apps. </p>
|
|
</div>
|
|
<div class="grid gap-1-5 card">
|
|
<div class="grid gap-0-5">
|
|
<h5>FLO ID</h5>
|
|
<sm-copy id="generated_flo_id"></sm-copy>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<h5>Private key</h5>
|
|
<sm-copy id="generated_private_key"></sm-copy>
|
|
</div>
|
|
</div>
|
|
<sm-button id="sign_up_button" variant="primary">Sign in with these credentials</sm-button>
|
|
<strong class="warning">
|
|
Keep your private key secure and don't share with anyone.
|
|
Once lost there is no way to recover private key.
|
|
</strong>
|
|
</section>
|
|
</sm-popup>
|
|
<sm-popup id="user_popup">
|
|
<header slot="header" class="popup__header">
|
|
<div class="flex align-center">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
<section class="grid gap-1-5">
|
|
<div class="grid gap-0-5">
|
|
<h5>My FLO ID</h5>
|
|
<sm-copy id="user_flo_id"></sm-copy>
|
|
</div>
|
|
<sm-button class="danger" onclick="signOut()">Sign out</sm-button>
|
|
</section>
|
|
</sm-popup>
|
|
|
|
<script id="floGlobals">
|
|
/* Constants for FLO blockchain operations !!Make sure to add this at begining!! */
|
|
const floGlobals = {
|
|
|
|
//Required for all
|
|
blockchain: "FLO",
|
|
|
|
//Required for blockchain API operators
|
|
apiURL: {
|
|
FLO: ['https://livenet.flocha.in/', 'https://flosight.duckdns.org/'],
|
|
FLO_TEST: ['https://testnet-flosight.duckdns.org/', 'https://testnet.flocha.in/']
|
|
},
|
|
adminID: "FF5pewfsJxyrCvg8a2C8VXefeyVvKvQxmF",
|
|
sendAmt: 0.001,
|
|
fee: 0.0005,
|
|
|
|
//Required for Supernode operations
|
|
SNStorageID: "FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk",
|
|
supernodes: {}, //each supnernode must be stored as floID : {uri:<uri>,pubKey:<publicKey>}
|
|
|
|
//for cloud apps
|
|
subAdmins: [],
|
|
application: "RM_Times",
|
|
appObjects: {},
|
|
generalData: {},
|
|
lastVC: {}
|
|
}
|
|
</script>
|
|
<script>
|
|
const slideInLeft = [
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(1rem)'
|
|
},
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
}
|
|
]
|
|
const slideOutLeft = [
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
},
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(-1rem)'
|
|
},
|
|
]
|
|
const slideInRight = [
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(-1rem)'
|
|
},
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
}
|
|
]
|
|
const slideOutRight = [
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateX(0)'
|
|
},
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateX(1rem)'
|
|
},
|
|
]
|
|
const slideInDown = [
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateY(-1rem)'
|
|
},
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateY(0)'
|
|
},
|
|
]
|
|
const slideOutUp = [
|
|
{
|
|
opacity: 1,
|
|
transform: 'translateY(0)'
|
|
},
|
|
{
|
|
opacity: 0,
|
|
transform: 'translateY(-1rem)'
|
|
},
|
|
]
|
|
// Global variables
|
|
const domRefs = {};
|
|
let timerId;
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
//Checks for internet connection status
|
|
if (!navigator.onLine)
|
|
notify(
|
|
"There seems to be a problem connecting to the internet, Please check you internet connection.",
|
|
"error",
|
|
{ sound: true }
|
|
);
|
|
window.addEventListener("offline", () => {
|
|
notify(
|
|
"There seems to be a problem connecting to the internet, Please check you internet connection.",
|
|
"error",
|
|
{ pinned: true, sound: true }
|
|
);
|
|
});
|
|
window.addEventListener("online", () => {
|
|
getRef("notification_drawer").clearAll();
|
|
notify("We are back online.", "success");
|
|
});
|
|
|
|
// Use instead of document.getElementById
|
|
function getRef(elementId) {
|
|
if (!domRefs.hasOwnProperty(elementId)) {
|
|
domRefs[elementId] = {
|
|
count: 1,
|
|
ref: null,
|
|
};
|
|
return document.getElementById(elementId);
|
|
} else {
|
|
if (domRefs[elementId].count < 3) {
|
|
domRefs[elementId].count = domRefs[elementId].count + 1;
|
|
return document.getElementById(elementId);
|
|
} else {
|
|
if (!domRefs[elementId].ref)
|
|
domRefs[elementId].ref = document.getElementById(elementId);
|
|
return domRefs[elementId].ref;
|
|
}
|
|
}
|
|
}
|
|
let zIndex = 50
|
|
// function required for popups or modals to appear
|
|
function openPopup(popupId, pinned) {
|
|
zIndex++
|
|
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
|
|
getRef(popupId).show({ pinned })
|
|
return getRef(popupId);
|
|
}
|
|
|
|
// hides the popup or modal
|
|
function closePopup() {
|
|
if (popupStack.peek() === undefined)
|
|
return;
|
|
popupStack.peek().popup.hide()
|
|
}
|
|
document.addEventListener('popupclosed', e => {
|
|
switch (e.target.id) {
|
|
case 'sign_in_popup':
|
|
getRef('sign_in').classList.remove('hide-completely')
|
|
getRef('sign_in').style = ''
|
|
getRef('sign_up').classList.add('hide-completely')
|
|
break
|
|
}
|
|
})
|
|
|
|
// displays a popup for asking permission. Use this instead of JS confirm
|
|
const getConfirmation = (title, options = {}) => {
|
|
return new Promise(resolve => {
|
|
const { message, cancelText = 'Cancel', confirmText = 'OK' } = options
|
|
openPopup('confirmation_popup', true)
|
|
getRef('confirm_title').textContent = title;
|
|
getRef('confirm_message').textContent = message;
|
|
let cancelButton = getRef('confirmation_popup').children[2].children[0],
|
|
submitButton = getRef('confirmation_popup').children[2].children[1]
|
|
submitButton.textContent = confirmText
|
|
cancelButton.textContent = cancelText
|
|
submitButton.onclick = () => {
|
|
closePopup()
|
|
resolve(true);
|
|
}
|
|
cancelButton.onclick = () => {
|
|
closePopup()
|
|
resolve(false);
|
|
}
|
|
})
|
|
}
|
|
let currentArticleID
|
|
window.addEventListener("load", () => {
|
|
currentArticleID = document.body.dataset.articleId
|
|
document.body.classList.remove('hide-completely')
|
|
document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex)
|
|
document.addEventListener('keyup', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closePopup()
|
|
}
|
|
})
|
|
document.addEventListener('copy', () => {
|
|
notify('copied', 'success')
|
|
})
|
|
});
|
|
function animateTo(element, keyframes, options) {
|
|
const anime = element.animate(keyframes, { ...options, fill: 'both' })
|
|
anime.finished.then(() => {
|
|
anime.commitStyles()
|
|
anime.cancel()
|
|
})
|
|
return anime
|
|
}
|
|
// Use when a function needs to be executed after user finishes changes
|
|
const debounce = (callback, wait) => {
|
|
let timeoutId = null;
|
|
return (...args) => {
|
|
window.clearTimeout(timeoutId);
|
|
timeoutId = window.setTimeout(() => {
|
|
callback.apply(null, args);
|
|
}, wait);
|
|
};
|
|
}
|
|
|
|
let tempVoteCount = 0
|
|
getRef('upvote_button').addEventListener('mouseup', function () {
|
|
if (isLoggedIn) {
|
|
tempVoteCount++;
|
|
const animOptions = {
|
|
fill: 'forwards',
|
|
duration: 300,
|
|
ease: 'easing',
|
|
}
|
|
const ring = document.createElement('div')
|
|
ring.classList.add('ring')
|
|
ring.animate([
|
|
{ transform: 'none' },
|
|
{ transform: 'scale(6)', opacity: 0 }
|
|
], animOptions).onfinish = e => {
|
|
e.target.cancel()
|
|
ring.remove()
|
|
}
|
|
this.append(ring)
|
|
this.firstElementChild.animate([
|
|
{ transform: 'scale(0.5)' },
|
|
{ transform: 'scale(1.4)', offset: 0.5 },
|
|
{ transform: 'none' },
|
|
], animOptions).onfinish = e => e.target.cancel()
|
|
}
|
|
})
|
|
function animateLikeCount(voteCount = 1, articleID) {
|
|
const animOptions = {
|
|
fill: 'forwards',
|
|
duration: 150,
|
|
ease: 'easing',
|
|
}
|
|
floGlobals.appObjects.rmTimes.articles[articleID].votes += voteCount
|
|
getRef('like_count').animate(slideOutUp, animOptions)
|
|
.onfinish = (e) => {
|
|
e.target.cancel()
|
|
}
|
|
const tempCount = document.createElement('div')
|
|
tempCount.classList.add('temp-count')
|
|
tempCount.textContent = floGlobals.appObjects.rmTimes.articles[articleID].votes
|
|
getRef('like_count').after(tempCount)
|
|
tempCount.animate(slideInUp, animOptions)
|
|
.onfinish = () => {
|
|
getRef('like_count').textContent = floGlobals.appObjects.rmTimes.articles[articleID].votes
|
|
tempCount.remove()
|
|
}
|
|
}
|
|
getRef('upvote_button').addEventListener('click', debounce(() => {
|
|
if (isLoggedIn) {
|
|
floCloudAPI.sendGeneralData({
|
|
voteCount: tempVoteCount,
|
|
}, `article_${currentArticleID}_votes`)
|
|
.then(res => {
|
|
tempVoteCount = 0
|
|
console.log('up voted')
|
|
})
|
|
.catch(err => console.log(err))
|
|
} else {
|
|
openPopup('sign_in_popup')
|
|
}
|
|
}, 300))
|
|
function generateCredentials() {
|
|
const animOptions = {
|
|
fill: 'forwards',
|
|
duration: 150,
|
|
ease: 'easing',
|
|
}
|
|
const { floID, privKey } = floCrypto.generateNewID()
|
|
getRef('generated_flo_id').value = floID
|
|
getRef('generated_private_key').value = privKey
|
|
animateTo(getRef('sign_in'), slideOutLeft, animOptions).onfinish = () => {
|
|
getRef('sign_in').classList.add('hide-completely')
|
|
getRef('sign_up').classList.remove('hide-completely')
|
|
animateTo(getRef('sign_up'), slideInLeft, animOptions)
|
|
}
|
|
}
|
|
function goToSignIn() {
|
|
const animOptions = {
|
|
fill: 'forwards',
|
|
duration: 150,
|
|
ease: 'easing',
|
|
}
|
|
animateTo(getRef('sign_up'), slideOutRight, animOptions).onfinish = () => {
|
|
getRef('sign_in').classList.remove('hide-completely')
|
|
animateTo(getRef('sign_in'), slideInRight, animOptions)
|
|
getRef('sign_up').classList.add('hide-completely')
|
|
}
|
|
}
|
|
function signOut() {
|
|
getConfirmation('Sign out?', 'You are about to sign out of the app, continue?', 'Stay', 'Leave')
|
|
.then(async (res) => {
|
|
if (res) {
|
|
await floDapps.clearCredentials()
|
|
getRef('user_button').classList.add('hide-completely')
|
|
location.reload()
|
|
}
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<script id="onLoadStartUp">
|
|
floGlobals.isSubAdmin = false
|
|
let isLoggedIn = false
|
|
function onLoadStartUp() {
|
|
//floDapps.addStartUpFunction('Sample', Promised Function)
|
|
//floDapps.setAppObjectStores({sampleObs1:{}, sampleObs2:{options{autoIncrement:true, keyPath:'SampleKey'}, Indexes:{sampleIndex:{}}}})
|
|
let firstLoad = true
|
|
floDapps.setMidStartup(() => new Promise(async (resolve, reject) => {
|
|
await floCloudAPI.requestObjectData('rmTimes')
|
|
await floCloudAPI.requestGeneralData(`article_${currentArticleID}_votes`, {
|
|
lowerVectorClock: floGlobals.appObjects.rmTimes.articles[currentArticleID].lastCountedVC + 1,
|
|
callback: (allVotes, e) => {
|
|
if (firstLoad) {
|
|
for (const vote in allVotes) {
|
|
floGlobals.appObjects.rmTimes.articles[currentArticleID].votes += allVotes[vote].message.voteCount || 1
|
|
}
|
|
getRef('like_count').textContent = floGlobals.appObjects.rmTimes.articles[currentArticleID].votes
|
|
} else {
|
|
for (const msg in allVotes) {
|
|
animateLikeCount(allVotes[msg].message.voteCount, currentArticleID)
|
|
}
|
|
}
|
|
firstLoad = false
|
|
}
|
|
})
|
|
resolve(true)
|
|
}))
|
|
floDapps.setCustomPrivKeyInput(() => new Promise((resolve, reject) => {
|
|
getRef('sign_in_button').onclick = () => {
|
|
resolve(getRef('private_key_field').value.trim())
|
|
getRef('private_key_field').value = ''
|
|
closePopup()
|
|
}
|
|
getRef('sign_up_button').onclick = () => {
|
|
resolve(getRef('generated_private_key').value.trim())
|
|
getRef('generated_private_key').value = ''
|
|
closePopup()
|
|
}
|
|
}))
|
|
|
|
|
|
floDapps.launchStartUp().then(async result => {
|
|
isLoggedIn = true
|
|
floGlobals.isSubAdmin = floGlobals.subAdmins.includes(myFloID)
|
|
|
|
getRef('user_flo_id').value = myFloID
|
|
getRef('user_button').classList.remove('hide-completely')
|
|
console.log(result)
|
|
}).catch(error => console.error(error))
|
|
}
|
|
</script>
|
|
<script>
|
|
const smButton = document.createElement('template')
|
|
smButton.innerHTML = `
|
|
<style>
|
|
*{
|
|
padding: 0;
|
|
margin: 0;
|
|
-webkit-box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
}
|
|
:host{
|
|
display: -webkit-inline-box;
|
|
display: -ms-inline-flexbox;
|
|
display: inline-flex;
|
|
width: auto;
|
|
--accent-color: #4d2588;
|
|
--text-color: 17, 17, 17;
|
|
--background-color: 255, 255, 255;
|
|
--padding: 0.6rem 1.2rem;
|
|
--border-radius: 0.3rem;
|
|
--background: rgba(var(--text-color), 0.1);
|
|
}
|
|
:host([variant='primary']) .button{
|
|
background: var(--accent-color);
|
|
color: rgba(var(--background-color), 1);
|
|
}
|
|
:host([variant='outlined']) .button{
|
|
-webkit-box-shadow: 0 0 0 1px rgba(var(--text-color), 0.2) inset;
|
|
box-shadow: 0 0 0 1px rgba(var(--text-color), 0.2) inset;
|
|
background: transparent;
|
|
color: var(--accent-color);
|
|
}
|
|
:host([variant='no-outline']) .button{
|
|
background: inherit;
|
|
color: var(--accent-color);
|
|
}
|
|
:host([disabled]){
|
|
pointer-events: none;
|
|
cursor: not-allowed;
|
|
}
|
|
.button {
|
|
position: relative;
|
|
display: -webkit-box;
|
|
display: -ms-flexbox;
|
|
display: flex;
|
|
width: 100%;
|
|
padding: var(--padding);
|
|
cursor: pointer;
|
|
-webkit-user-select: none;
|
|
-moz-user-select: none;
|
|
-ms-user-select: none;
|
|
user-select: none;
|
|
border-radius: var(--border-radius);
|
|
-webkit-box-pack: center;
|
|
-ms-flex-pack: center;
|
|
justify-content: center;
|
|
transition: box-shadow 0.3s, background-color 0.3s;
|
|
font-family: inherit;
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
background-color: var(--background);
|
|
-webkit-tap-highlight-color: transparent;
|
|
outline: none;
|
|
overflow: hidden;
|
|
border: none;
|
|
color: inherit;
|
|
align-items: center;
|
|
}
|
|
:host([disabled]) .button{
|
|
pointer-events: none;
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
color: rgba(var(--text-color), 1);
|
|
background-color: rgba(var(--text-color), 0.3);
|
|
}
|
|
@media (hover: hover){
|
|
:host(:not([disabled])) .button:hover,
|
|
:host(:focus-within:not([disabled])) .button{
|
|
-webkit-box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.2rem 0.8rem rgba(0, 0, 0, 0.12);
|
|
box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.2rem 0.8rem rgba(0, 0, 0, 0.12);
|
|
}
|
|
:host([variant='outlined']:not([disabled])) .button:hover,
|
|
:host(:focus-within[variant='outlined']:not([disabled])) .button:hover{
|
|
-webkit-box-shadow: 0 0 0 1px rgba(var(--text-color), 0.2) inset, 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.4rem 0.8rem rgba(0, 0, 0, 0.12);
|
|
box-shadow: 0 0 0 1px rgba(var(--text-color), 0.2) inset, 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.4rem 0.8rem rgba(0, 0, 0, 0.12);
|
|
}
|
|
}
|
|
@media (hover: none){
|
|
:host(:not([disabled])) .button:active{
|
|
-webkit-box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.2rem 0.8rem rgba(0, 0, 0, 0.2);
|
|
box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.2rem 0.8rem rgba(0, 0, 0, 0.2);
|
|
}
|
|
:host([variant='outlined']) .button:active{
|
|
-webkit-box-shadow: 0 0 0 1px rgba(var(--text-color), 0.2) inset, 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.4rem 0.8rem rgba(0, 0, 0, 0.2);
|
|
box-shadow: 0 0 0 1px rgba(var(--text-color), 0.2) inset, 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.4rem 0.8rem rgba(0, 0, 0, 0.2);
|
|
}
|
|
}
|
|
</style>
|
|
<div part="button" class="button">
|
|
<slot></slot>
|
|
</div>`;
|
|
customElements.define('sm-button',
|
|
class extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({
|
|
mode: 'open'
|
|
}).append(smButton.content.cloneNode(true));
|
|
}
|
|
static get observedAttributes() {
|
|
return ['disabled'];
|
|
}
|
|
|
|
get disabled() {
|
|
return this.hasAttribute('disabled');
|
|
}
|
|
|
|
set disabled(value) {
|
|
if (value) {
|
|
this.setAttribute('disabled', '');
|
|
} else {
|
|
this.removeAttribute('disabled');
|
|
}
|
|
}
|
|
focusIn() {
|
|
this.focus();
|
|
}
|
|
|
|
handleKeyDown(e) {
|
|
if (!this.hasAttribute('disabled') && (e.key === 'Enter' || e.key === ' ')) {
|
|
e.preventDefault();
|
|
this.click();
|
|
}
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (!this.hasAttribute('disabled')) {
|
|
this.setAttribute('tabindex', '0');
|
|
}
|
|
this.setAttribute('role', 'button');
|
|
this.addEventListener('keydown', this.handleKeyDown);
|
|
}
|
|
attributeChangedCallback(name) {
|
|
if (name === 'disabled') {
|
|
if (this.hasAttribute('disabled')) {
|
|
this.removeAttribute('tabindex');
|
|
} else {
|
|
this.setAttribute('tabindex', '0');
|
|
}
|
|
this.setAttribute('aria-disabled', this.hasAttribute('disabled'));
|
|
}
|
|
}
|
|
})
|
|
const smInput = document.createElement('template')
|
|
smInput.innerHTML = `
|
|
<style>
|
|
*{
|
|
padding: 0;
|
|
margin: 0;
|
|
-webkit-box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
}
|
|
input[type="search"]::-webkit-search-decoration,
|
|
input[type="search"]::-webkit-search-cancel-button,
|
|
input[type="search"]::-webkit-search-results-button,
|
|
input[type="search"]::-webkit-search-results-decoration { display: none; }
|
|
input[type=number] {
|
|
-moz-appearance:textfield;
|
|
}
|
|
input[type=number]::-webkit-inner-spin-button,
|
|
input[type=number]::-webkit-outer-spin-button {
|
|
-webkit-appearance: none;
|
|
-moz-appearance: none;
|
|
appearance: none;
|
|
margin: 0;
|
|
}
|
|
input::-ms-reveal,
|
|
input::-ms-clear {
|
|
display: none;
|
|
}
|
|
input:invalid{
|
|
outline: none;
|
|
-webkit-box-shadow: none;
|
|
box-shadow: none;
|
|
}
|
|
::-moz-focus-inner{
|
|
border: none;
|
|
}
|
|
:host{
|
|
display: -webkit-box;
|
|
display: -ms-flexbox;
|
|
display: flex;
|
|
--accent-color: #4d2588;
|
|
--text-color: 17, 17, 17;
|
|
--background-color: 255, 255, 255;
|
|
--success-color: #00C853;
|
|
--danger-color: red;
|
|
--width: 100%;
|
|
--icon-gap: 0.5rem;
|
|
--border-radius: 0.3rem;
|
|
--padding: 0.7rem 1rem;
|
|
--background: rgba(var(--text-color), 0.06);
|
|
}
|
|
.hide{
|
|
opacity: 0 !important;
|
|
pointer-events: none !important;
|
|
}
|
|
.hide-completely{
|
|
display: none;
|
|
}
|
|
.icon {
|
|
fill: rgba(var(--text-color), 0.6);
|
|
height: 1.4rem;
|
|
width: 1.4rem;
|
|
border-radius: 1rem;
|
|
cursor: pointer;
|
|
min-width: 0;
|
|
}
|
|
|
|
:host(.round) .input{
|
|
border-radius: 10rem;
|
|
}
|
|
.input {
|
|
display: -webkit-box;
|
|
display: -ms-flexbox;
|
|
display: flex;
|
|
cursor: text;
|
|
min-width: 0;
|
|
text-align: left;
|
|
-webkit-box-align: center;
|
|
-ms-flex-align: center;
|
|
align-items: center;
|
|
position: relative;
|
|
gap: var(--icon-gap);
|
|
padding: var(--padding);
|
|
border-radius: var(--border-radius);
|
|
-webkit-transition: opacity 0.3s;
|
|
-o-transition: opacity 0.3s;
|
|
transition: opacity 0.3s;
|
|
background: var(--background);
|
|
width: 100%;
|
|
outline: none;
|
|
}
|
|
.input.readonly .clear{
|
|
opacity: 0 !important;
|
|
margin-right: -2rem;
|
|
pointer-events: none !important;
|
|
}
|
|
.readonly{
|
|
pointer-events: none;
|
|
}
|
|
.input:focus-within:not(.readonly){
|
|
box-shadow: 0 0 0 0.1rem var(--accent-color) inset !important;
|
|
}
|
|
.disabled{
|
|
pointer-events: none;
|
|
opacity: 0.6;
|
|
}
|
|
.label {
|
|
font-size: inherit;
|
|
opacity: .7;
|
|
font-weight: 400;
|
|
position: absolute;
|
|
top: 0;
|
|
-webkit-transition: -webkit-transform 0.3s;
|
|
transition: -webkit-transform 0.3s;
|
|
-o-transition: transform 0.3s;
|
|
transition: transform 0.3s;
|
|
transition: transform 0.3s, -webkit-transform 0.3s;
|
|
-webkit-transform-origin: left;
|
|
-ms-transform-origin: left;
|
|
transform-origin: left;
|
|
pointer-events: none;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
-o-text-overflow: ellipsis;
|
|
text-overflow: ellipsis;
|
|
width: 100%;
|
|
user-select: none;
|
|
will-change: transform;
|
|
}
|
|
.outer-container{
|
|
position: relative;
|
|
width: var(--width);
|
|
}
|
|
.container{
|
|
width: 100%;
|
|
display: -webkit-box;
|
|
display: -ms-flexbox;
|
|
display: flex;
|
|
position: relative;
|
|
-webkit-box-align: center;
|
|
-ms-flex-align: center;
|
|
align-items: center;
|
|
-webkit-box-flex: 1;
|
|
-ms-flex: 1;
|
|
flex: 1;
|
|
}
|
|
input{
|
|
font-size: inherit;
|
|
border: none;
|
|
background: transparent;
|
|
outline: none;
|
|
color: rgba(var(--text-color), 1);
|
|
width: 100%;
|
|
}
|
|
:host(:not([variant="outlined"])) .animate-label .container input {
|
|
-webkit-transform: translateY(0.6rem);
|
|
-ms-transform: translateY(0.6rem);
|
|
transform: translateY(0.6rem);
|
|
}
|
|
|
|
:host(:not([variant="outlined"])) .animate-label .label {
|
|
-webkit-transform: translateY(-0.7em) scale(0.8);
|
|
-ms-transform: translateY(-0.7em) scale(0.8);
|
|
transform: translateY(-0.7em) scale(0.8);
|
|
opacity: 1;
|
|
color: var(--accent-color)
|
|
}
|
|
:host([variant="outlined"]) .input {
|
|
box-shadow: 0 0 0 0.1rem var(--border-color, rgba(var(--text-color), 0.4)) inset;
|
|
background: rgba(var(--background-color), 1);
|
|
}
|
|
:host([variant="outlined"]) .label {
|
|
width: max-content;
|
|
margin-left: -0.5rem;
|
|
padding: 0 0.5rem;
|
|
}
|
|
:host([variant="outlined"]) .animate-label .label {
|
|
-webkit-transform: translate(0.1rem, -1.5rem) scale(0.8);
|
|
-ms-transform: translate(0.1rem, -1.5rem) scale(0.8);
|
|
transform: translate(0.1rem, -1.5rem) scale(0.8);
|
|
opacity: 1;
|
|
background: rgba(var(--background-color), 1);
|
|
}
|
|
.animate-label:focus-within:not(.readonly) .label{
|
|
color: var(--accent-color)
|
|
}
|
|
.feedback-text:not(:empty){
|
|
display: flex;
|
|
width: 100%;
|
|
text-align: left;
|
|
font-size: 0.9rem;
|
|
align-items: center;
|
|
padding: 0.8rem 0;
|
|
color: rgba(var(--text-color), 0.8);
|
|
}
|
|
.success{
|
|
color: var(--success-color);
|
|
}
|
|
.error{
|
|
color: var(--danger-color);
|
|
}
|
|
.status-icon{
|
|
margin-right: 0.2rem;
|
|
}
|
|
.status-icon--error{
|
|
fill: var(--danger-color);
|
|
}
|
|
.status-icon--success{
|
|
fill: var(--success-color);
|
|
}
|
|
@media (any-hover: hover){
|
|
.icon:hover{
|
|
background: rgba(var(--text-color), 0.1);
|
|
}
|
|
}
|
|
</style>
|
|
<div class="outer-container">
|
|
<label part="input" class="input">
|
|
<slot name="icon"></slot>
|
|
<div class="container">
|
|
<input type="text"/>
|
|
<div part="placeholder" class="label"></div>
|
|
</div>
|
|
<svg class="icon clear hide" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-11.414L9.172 7.757 7.757 9.172 10.586 12l-2.829 2.828 1.415 1.415L12 13.414l2.828 2.829 1.415-1.415L13.414 12l2.829-2.828-1.415-1.415L12 10.586z"/></svg>
|
|
</label>
|
|
<p class="feedback-text"></p>
|
|
</div>
|
|
`;
|
|
customElements.define('sm-input',
|
|
class extends HTMLElement {
|
|
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({
|
|
mode: 'open'
|
|
}).append(smInput.content.cloneNode(true));
|
|
|
|
this.inputParent = this.shadowRoot.querySelector('.input');
|
|
this.input = this.shadowRoot.querySelector('input');
|
|
this.clearBtn = this.shadowRoot.querySelector('.clear');
|
|
this.label = this.shadowRoot.querySelector('.label');
|
|
this.feedbackText = this.shadowRoot.querySelector('.feedback-text');
|
|
this.outerContainer = this.shadowRoot.querySelector('.outer-container');
|
|
this._helperText = '';
|
|
this._errorText = '';
|
|
this.isRequired = false;
|
|
this.hideRequired = false;
|
|
this.validationFunction = undefined;
|
|
this.reflectedAttributes = ['value', 'required', 'disabled', 'type', 'inputmode', 'readonly', 'min', 'max', 'pattern', 'minlength', 'maxlength', 'step'];
|
|
|
|
this.reset = this.reset.bind(this);
|
|
this.focusIn = this.focusIn.bind(this);
|
|
this.focusOut = this.focusOut.bind(this);
|
|
this.fireEvent = this.fireEvent.bind(this);
|
|
this.checkInput = this.checkInput.bind(this);
|
|
this.vibrate = this.vibrate.bind(this);
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return ['value', 'placeholder', 'required', 'disabled', 'type', 'inputmode', 'readonly', 'min', 'max', 'pattern', 'minlength', 'maxlength', 'step', 'helper-text', 'error-text', 'hiderequired'];
|
|
}
|
|
|
|
get value() {
|
|
return this.input.value;
|
|
}
|
|
|
|
set value(val) {
|
|
this.input.value = val;
|
|
this.checkInput();
|
|
this.fireEvent();
|
|
}
|
|
|
|
get placeholder() {
|
|
return this.getAttribute('placeholder');
|
|
}
|
|
|
|
set placeholder(val) {
|
|
this.setAttribute('placeholder', val);
|
|
}
|
|
|
|
get type() {
|
|
return this.getAttribute('type');
|
|
}
|
|
|
|
set type(val) {
|
|
this.setAttribute('type', val);
|
|
}
|
|
|
|
get validity() {
|
|
return this.input.validity;
|
|
}
|
|
|
|
get disabled() {
|
|
return this.hasAttribute('disabled');
|
|
}
|
|
set disabled(value) {
|
|
if (value)
|
|
this.inputParent.classList.add('disabled');
|
|
else
|
|
this.inputParent.classList.remove('disabled');
|
|
}
|
|
get readOnly() {
|
|
return this.hasAttribute('readonly');
|
|
}
|
|
set readOnly(value) {
|
|
if (value) {
|
|
this.setAttribute('readonly', '');
|
|
} else {
|
|
this.removeAttribute('readonly');
|
|
}
|
|
}
|
|
set customValidation(val) {
|
|
this.validationFunction = val;
|
|
}
|
|
set errorText(val) {
|
|
this._errorText = val;
|
|
}
|
|
set helperText(val) {
|
|
this._helperText = val;
|
|
}
|
|
get isValid() {
|
|
if (this.input.value !== '') {
|
|
const _isValid = this.input.checkValidity();
|
|
let _customValid = true;
|
|
if (this.validationFunction) {
|
|
_customValid = Boolean(this.validationFunction(this.input.value));
|
|
}
|
|
if (_isValid && _customValid) {
|
|
this.feedbackText.classList.remove('error');
|
|
this.feedbackText.classList.add('success');
|
|
this.feedbackText.textContent = '';
|
|
} else {
|
|
if (this._errorText) {
|
|
this.feedbackText.classList.add('error');
|
|
this.feedbackText.classList.remove('success');
|
|
this.feedbackText.innerHTML = `
|
|
<svg class="status-icon status-icon--error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>
|
|
${this._errorText}
|
|
`;
|
|
}
|
|
}
|
|
return (_isValid && _customValid);
|
|
}
|
|
}
|
|
reset() {
|
|
this.value = '';
|
|
}
|
|
|
|
focusIn() {
|
|
this.input.focus();
|
|
}
|
|
|
|
focusOut() {
|
|
this.input.blur();
|
|
}
|
|
|
|
fireEvent() {
|
|
let event = new Event('input', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
composed: true
|
|
});
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
checkInput(e) {
|
|
if (!this.hasAttribute('readonly')) {
|
|
if (this.input.value.trim() !== '') {
|
|
this.clearBtn.classList.remove('hide');
|
|
} else {
|
|
this.clearBtn.classList.add('hide');
|
|
if (this.isRequired && !this.hideRequired) {
|
|
this.feedbackText.textContent = '*required';
|
|
}
|
|
}
|
|
}
|
|
if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder').trim() === '') return;
|
|
if (this.input.value !== '') {
|
|
if (this.animate)
|
|
this.inputParent.classList.add('animate-label');
|
|
else
|
|
this.label.classList.add('hide');
|
|
} else {
|
|
if (this.animate)
|
|
this.inputParent.classList.remove('animate-label');
|
|
else
|
|
this.label.classList.remove('hide');
|
|
}
|
|
}
|
|
vibrate() {
|
|
this.outerContainer.animate([
|
|
{ transform: 'translateX(-1rem)' },
|
|
{ transform: 'translateX(1rem)' },
|
|
{ transform: 'translateX(-0.5rem)' },
|
|
{ transform: 'translateX(0.5rem)' },
|
|
{ transform: 'translateX(0)' },
|
|
], {
|
|
duration: 300,
|
|
easing: 'ease'
|
|
});
|
|
}
|
|
|
|
|
|
connectedCallback() {
|
|
this.animate = this.hasAttribute('animate');
|
|
this.setAttribute('role', 'textbox');
|
|
this.input.addEventListener('input', this.checkInput);
|
|
this.clearBtn.addEventListener('click', this.reset);
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (oldValue !== newValue) {
|
|
if (this.reflectedAttributes.includes(name)) {
|
|
if (this.hasAttribute(name)) {
|
|
this.input.setAttribute(name, this.getAttribute(name) ? this.getAttribute(name) : '');
|
|
}
|
|
else {
|
|
this.input.removeAttribute(name);
|
|
}
|
|
}
|
|
if (name === 'placeholder') {
|
|
this.label.textContent = newValue;
|
|
this.setAttribute('aria-label', newValue);
|
|
}
|
|
else if (this.hasAttribute('value')) {
|
|
this.checkInput();
|
|
}
|
|
else if (name === 'type') {
|
|
if (this.hasAttribute('type') && this.getAttribute('type') === 'number') {
|
|
this.input.setAttribute('inputmode', 'numeric');
|
|
}
|
|
}
|
|
else if (name === 'helper-text') {
|
|
this._helperText = this.getAttribute('helper-text');
|
|
}
|
|
else if (name === 'error-text') {
|
|
this._errorText = this.getAttribute('error-text');
|
|
}
|
|
else if (name === 'required') {
|
|
this.isRequired = this.hasAttribute('required');
|
|
if (this.isRequired && !this.hideRequired) {
|
|
this.feedbackText.textContent = '';
|
|
} else {
|
|
this.feedbackText.textContent = '*required';
|
|
}
|
|
if (this.isRequired) {
|
|
this.setAttribute('aria-required', 'true');
|
|
}
|
|
else {
|
|
this.setAttribute('aria-required', 'false');
|
|
}
|
|
}
|
|
else if (name === 'hiderequired') {
|
|
this.hideRequired = this.hasAttribute('hiderequired')
|
|
}
|
|
else if (name === 'readonly') {
|
|
if (this.hasAttribute('readonly')) {
|
|
this.inputParent.classList.add('readonly');
|
|
} else {
|
|
this.inputParent.classList.remove('readonly');
|
|
}
|
|
}
|
|
else if (name === 'disabled') {
|
|
if (this.hasAttribute('disabled')) {
|
|
this.inputParent.classList.add('disabled');
|
|
}
|
|
else {
|
|
this.inputParent.classList.remove('disabled');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
disconnectedCallback() {
|
|
this.input.removeEventListener('input', this.checkInput);
|
|
this.clearBtn.removeEventListener('click', this.reset);
|
|
}
|
|
})
|
|
|
|
//popup
|
|
class Stack {
|
|
constructor() {
|
|
this.items = [];
|
|
}
|
|
push(element) {
|
|
this.items.push(element);
|
|
}
|
|
pop() {
|
|
if (this.items.length == 0)
|
|
return "Underflow";
|
|
return this.items.pop();
|
|
}
|
|
peek() {
|
|
return this.items[this.items.length - 1];
|
|
}
|
|
}
|
|
const popupStack = new Stack();
|
|
|
|
const smPopup = document.createElement('template');
|
|
smPopup.innerHTML = `
|
|
<style>
|
|
*{
|
|
padding: 0;
|
|
margin: 0;
|
|
-webkit-box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
}
|
|
:host{
|
|
position: fixed;
|
|
display: -ms-grid;
|
|
display: grid;
|
|
z-index: 10;
|
|
--width: 100%;
|
|
--height: auto;
|
|
--min-width: auto;
|
|
--min-height: auto;
|
|
--backdrop-background: rgba(0, 0, 0, 0.6);
|
|
--border-radius: 0.8rem 0.8rem 0 0;
|
|
}
|
|
.popup-container{
|
|
display: -ms-grid;
|
|
display: grid;
|
|
position: fixed;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
place-items: center;
|
|
z-index: 10;
|
|
touch-action: none;
|
|
}
|
|
:host(.stacked) .popup{
|
|
-webkit-transform: scale(0.9) translateY(-2rem) !important;
|
|
transform: scale(0.9) translateY(-2rem) !important;
|
|
}
|
|
.background{
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
pointer-events: none;
|
|
background: var(--backdrop-background);
|
|
-webkit-transition: opacity 0.3s;
|
|
-o-transition: opacity 0.3s;
|
|
transition: opacity 0.3s;
|
|
}
|
|
.popup{
|
|
display: -webkit-box;
|
|
display: -ms-flexbox;
|
|
display: flex;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-box-direction: normal;
|
|
flex-direction: column;
|
|
position: relative;
|
|
-ms-flex-item-align: end;
|
|
align-self: flex-end;
|
|
-webkit-box-align: start;
|
|
-ms-flex-align: start;
|
|
align-items: flex-start;
|
|
width: var(--width);
|
|
min-width: var(--min-width);
|
|
height: var(--height);
|
|
min-height: var(--min-height);
|
|
max-height: 90vh;
|
|
border-radius: var(--border-radius);
|
|
background: rgba(var(--background-color, (255,255,255)), 1);
|
|
-webkit-box-shadow: 0 -1rem 2rem #00000020;
|
|
box-shadow: 0 -1rem 2rem #00000020;
|
|
}
|
|
.container-header{
|
|
display: -webkit-box;
|
|
display: flex;
|
|
width: 100%;
|
|
touch-action: none;
|
|
-webkit-box-align: center;
|
|
-ms-flex-align: center;
|
|
align-items: center;
|
|
}
|
|
.popup-top{
|
|
display: -webkit-box;
|
|
display: flex;
|
|
width: 100%;
|
|
}
|
|
.popup-body{
|
|
display: -webkit-box;
|
|
display: flex;
|
|
-webkit-box-orient: vertical;
|
|
-webkit-box-direction: normal;
|
|
-ms-flex-direction: column;
|
|
flex-direction: column;
|
|
-webkit-box-flex: 1;
|
|
-ms-flex: 1;
|
|
flex: 1;
|
|
width: 100%;
|
|
padding: var(--body-padding, 1.5rem);
|
|
overflow-y: auto;
|
|
}
|
|
.hide{
|
|
display:none;
|
|
}
|
|
@media screen and (min-width: 640px){
|
|
:host{
|
|
--border-radius: 0.5rem;
|
|
}
|
|
.popup{
|
|
-ms-flex-item-align: center;
|
|
-ms-grid-row-align: center;
|
|
align-self: center;
|
|
border-radius: var(--border-radius);
|
|
height: var(--height);
|
|
-webkit-box-shadow: 0 3rem 2rem -0.5rem #00000040;
|
|
box-shadow: 0 3rem 2rem -0.5rem #00000040;
|
|
}
|
|
}
|
|
@media screen and (max-width: 640px){
|
|
.popup-top{
|
|
-webkit-box-orient: vertical;
|
|
-webkit-box-direction: normal;
|
|
flex-direction: column;
|
|
-webkit-box-align: center;
|
|
align-items: center;
|
|
}
|
|
.handle{
|
|
height: 0.3rem;
|
|
width: 2rem;
|
|
background: rgba(var(--text-color, (17,17,17)), .4);
|
|
border-radius: 1rem;
|
|
margin: 0.5rem 0;
|
|
}
|
|
}
|
|
@media (any-hover: hover){
|
|
::-webkit-scrollbar{
|
|
width: 0.5rem;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb{
|
|
background: rgba(var(--text-color, (17,17,17)), 0.3);
|
|
border-radius: 1rem;
|
|
&:hover{
|
|
background: rgba(var(--text-color, (17,17,17))), 0.5);
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
<div class="popup-container hide" role="dialog">
|
|
<div part="background" class="background"></div>
|
|
<div part="popup" class="popup">
|
|
<div part="popup-header" class="popup-top">
|
|
<div class="handle"></div>
|
|
<slot name="header"></slot>
|
|
</div>
|
|
<div part="popup-body" class="popup-body">
|
|
<slot></slot>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
customElements.define('sm-popup', class extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({
|
|
mode: 'open'
|
|
}).append(smPopup.content.cloneNode(true));
|
|
|
|
this.allowClosing = false;
|
|
this.isOpen = false;
|
|
this.pinned = false;
|
|
this.offset = 0;
|
|
this.touchStartY = 0;
|
|
this.touchEndY = 0;
|
|
this.touchStartTime = 0;
|
|
this.touchEndTime = 0;
|
|
this.touchEndAnimation = undefined;
|
|
this.focusable
|
|
this.autoFocus
|
|
this.mutationObserver
|
|
|
|
this.popupContainer = this.shadowRoot.querySelector('.popup-container');
|
|
this.backdrop = this.shadowRoot.querySelector('.background');
|
|
this.dialogBox = this.shadowRoot.querySelector('.popup');
|
|
this.popupBodySlot = this.shadowRoot.querySelector('.popup-body slot');
|
|
this.popupHeader = this.shadowRoot.querySelector('.popup-top');
|
|
|
|
this.resumeScrolling = this.resumeScrolling.bind(this);
|
|
this.setStateOpen = this.setStateOpen.bind(this);
|
|
this.show = this.show.bind(this);
|
|
this.hide = this.hide.bind(this);
|
|
this.handleTouchStart = this.handleTouchStart.bind(this);
|
|
this.handleTouchMove = this.handleTouchMove.bind(this);
|
|
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
|
this.detectFocus = this.detectFocus.bind(this);
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return ['open'];
|
|
}
|
|
|
|
get open() {
|
|
return this.isOpen;
|
|
}
|
|
|
|
animateTo(element, keyframes, options) {
|
|
const anime = element.animate(keyframes, { ...options, fill: 'both' })
|
|
anime.finished.then(() => {
|
|
anime.commitStyles()
|
|
anime.cancel()
|
|
})
|
|
return anime
|
|
}
|
|
|
|
resumeScrolling() {
|
|
const scrollY = document.body.style.top;
|
|
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
|
document.body.style.overflow = '';
|
|
document.body.style.top = 'initial';
|
|
}
|
|
|
|
setStateOpen() {
|
|
if (!this.isOpen || this.offset) {
|
|
const animOptions = {
|
|
duration: 300,
|
|
easing: 'ease'
|
|
}
|
|
const initialAnimation = (window.innerWidth > 640) ? 'scale(1.1)' : `translateY(${this.offset ? `${this.offset}px` : '100%'})`
|
|
this.animateTo(this.dialogBox, [
|
|
{
|
|
opacity: this.offset ? 1 : 0,
|
|
transform: initialAnimation
|
|
},
|
|
{
|
|
opacity: 1,
|
|
transform: 'none'
|
|
},
|
|
], animOptions)
|
|
|
|
}
|
|
}
|
|
|
|
show(options = {}) {
|
|
const { pinned = false } = options;
|
|
if (!this.isOpen) {
|
|
const animOptions = {
|
|
duration: 300,
|
|
easing: 'ease'
|
|
}
|
|
popupStack.push({
|
|
popup: this,
|
|
permission: pinned
|
|
});
|
|
if (popupStack.items.length > 1) {
|
|
this.animateTo(popupStack.items[popupStack.items.length - 2].popup.shadowRoot.querySelector('.popup'), [
|
|
{ transform: 'none' },
|
|
{ transform: (window.innerWidth > 640) ? 'scale(0.95)' : 'translateY(-1.5rem)' },
|
|
], animOptions)
|
|
}
|
|
this.popupContainer.classList.remove('hide');
|
|
if (!this.offset)
|
|
this.backdrop.animate([
|
|
{ opacity: 0 },
|
|
{ opacity: 1 },
|
|
], animOptions)
|
|
this.setStateOpen()
|
|
this.dispatchEvent(
|
|
new CustomEvent("popupopened", {
|
|
bubbles: true,
|
|
detail: {
|
|
popup: this,
|
|
}
|
|
})
|
|
);
|
|
this.pinned = pinned;
|
|
this.isOpen = true;
|
|
document.body.style.overflow = 'hidden';
|
|
document.body.style.top = `-${window.scrollY}px`;
|
|
const elementToFocus = this.autoFocus || this.focusable[0];
|
|
elementToFocus.tagName.includes('SM-') ? elementToFocus.focusIn() : elementToFocus.focus();
|
|
if (!this.hasAttribute('open'))
|
|
this.setAttribute('open', '');
|
|
}
|
|
}
|
|
hide() {
|
|
const animOptions = {
|
|
duration: 150,
|
|
easing: 'ease'
|
|
}
|
|
this.backdrop.animate([
|
|
{ opacity: 1 },
|
|
{ opacity: 0 }
|
|
], animOptions)
|
|
this.animateTo(this.dialogBox, [
|
|
{
|
|
opacity: 1,
|
|
transform: (window.innerWidth > 640) ? 'none' : `translateY(${this.offset ? `${this.offset}px` : '0'})`
|
|
},
|
|
{
|
|
opacity: 0,
|
|
transform: (window.innerWidth > 640) ? 'scale(1.1)' : 'translateY(100%)'
|
|
},
|
|
], animOptions).finished
|
|
.finally(() => {
|
|
this.popupContainer.classList.add('hide');
|
|
this.dialogBox.style = ''
|
|
this.removeAttribute('open');
|
|
|
|
if (this.forms.length) {
|
|
this.forms.forEach(form => form.reset());
|
|
}
|
|
this.dispatchEvent(
|
|
new CustomEvent("popupclosed", {
|
|
bubbles: true,
|
|
detail: {
|
|
popup: this,
|
|
}
|
|
})
|
|
);
|
|
this.isOpen = false;
|
|
})
|
|
popupStack.pop();
|
|
if (popupStack.items.length) {
|
|
this.animateTo(popupStack.items[popupStack.items.length - 1].popup.shadowRoot.querySelector('.popup'), [
|
|
{ transform: (window.innerWidth > 640) ? 'scale(0.95)' : 'translateY(-1.5rem)' },
|
|
{ transform: 'none' },
|
|
], animOptions)
|
|
|
|
} else {
|
|
this.resumeScrolling();
|
|
}
|
|
}
|
|
|
|
handleTouchStart(e) {
|
|
this.offset = 0
|
|
this.popupHeader.addEventListener('touchmove', this.handleTouchMove, { passive: true });
|
|
this.popupHeader.addEventListener('touchend', this.handleTouchEnd, { passive: true });
|
|
this.touchStartY = e.changedTouches[0].clientY;
|
|
this.touchStartTime = e.timeStamp;
|
|
}
|
|
|
|
handleTouchMove(e) {
|
|
if (this.touchStartY < e.changedTouches[0].clientY) {
|
|
this.offset = e.changedTouches[0].clientY - this.touchStartY;
|
|
this.touchEndAnimation = window.requestAnimationFrame(() => {
|
|
this.dialogBox.style.transform = `translateY(${this.offset}px)`;
|
|
});
|
|
}
|
|
}
|
|
|
|
handleTouchEnd(e) {
|
|
this.touchEndTime = e.timeStamp;
|
|
cancelAnimationFrame(this.touchEndAnimation);
|
|
this.touchEndY = e.changedTouches[0].clientY;
|
|
this.threshold = this.dialogBox.getBoundingClientRect().height * 0.3;
|
|
if (this.touchEndTime - this.touchStartTime > 200) {
|
|
if (this.touchEndY - this.touchStartY > this.threshold) {
|
|
if (this.pinned) {
|
|
this.setStateOpen();
|
|
return;
|
|
} else
|
|
this.hide();
|
|
} else {
|
|
this.setStateOpen();
|
|
}
|
|
} else {
|
|
if (this.touchEndY > this.touchStartY)
|
|
if (this.pinned) {
|
|
this.setStateOpen();
|
|
return;
|
|
}
|
|
else
|
|
this.hide();
|
|
}
|
|
this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, { passive: true });
|
|
this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, { passive: true });
|
|
}
|
|
|
|
|
|
detectFocus(e) {
|
|
if (e.key === 'Tab') {
|
|
const lastElement = this.focusable[this.focusable.length - 1];
|
|
const firstElement = this.focusable[0];
|
|
if (e.shiftKey && document.activeElement === firstElement) {
|
|
e.preventDefault();
|
|
lastElement.tagName.includes('SM-') ? lastElement.focusIn() : lastElement.focus();
|
|
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
|
e.preventDefault();
|
|
firstElement.tagName.includes('SM-') ? firstElement.focusIn() : firstElement.focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
updateFocusableList() {
|
|
this.focusable = this.querySelectorAll('sm-button:not([disabled]), button:not([disabled]), [href], sm-input, input:not([readonly]), sm-select, select, sm-checkbox, sm-textarea, textarea, [tabindex]:not([tabindex="-1"])')
|
|
this.autoFocus = this.querySelector('[autofocus]')
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.popupBodySlot.addEventListener('slotchange', () => {
|
|
this.forms = this.querySelectorAll('sm-form');
|
|
this.updateFocusableList()
|
|
});
|
|
this.popupContainer.addEventListener('mousedown', e => {
|
|
if (e.target === this.popupContainer && !this.pinned) {
|
|
if (this.pinned) {
|
|
this.setStateOpen();
|
|
} else
|
|
this.hide();
|
|
}
|
|
});
|
|
|
|
const resizeObserver = new ResizeObserver(entries => {
|
|
for (let entry of entries) {
|
|
if (entry.contentBoxSize) {
|
|
// Firefox implements `contentBoxSize` as a single content rect, rather than an array
|
|
const contentBoxSize = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize;
|
|
this.threshold = contentBoxSize.blockSize.height * 0.3;
|
|
} else {
|
|
this.threshold = entry.contentRect.height * 0.3;
|
|
}
|
|
}
|
|
});
|
|
resizeObserver.observe(this);
|
|
|
|
this.mutationObserver = new MutationObserver(entries => {
|
|
this.updateFocusableList()
|
|
})
|
|
this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true })
|
|
|
|
this.addEventListener('keydown', this.detectFocus);
|
|
this.popupHeader.addEventListener('touchstart', this.handleTouchStart, { passive: true });
|
|
}
|
|
disconnectedCallback() {
|
|
this.removeEventListener('keydown', this.detectFocus);
|
|
resizeObserver.unobserve();
|
|
this.mutationObserver.disconnect()
|
|
this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
|
|
}
|
|
attributeChangedCallback(name) {
|
|
if (name === 'open') {
|
|
if (this.hasAttribute('open')) {
|
|
this.show();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const smCopy = document.createElement('template');
|
|
smCopy.innerHTML = `
|
|
<style>
|
|
*{
|
|
padding: 0;
|
|
margin: 0;
|
|
-webkit-box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
}
|
|
:host{
|
|
display: -webkit-box;
|
|
display: flex;
|
|
--accent-color: #4d2588;
|
|
--text-color: 17, 17, 17;
|
|
--background-color: 255, 255, 255;
|
|
--padding: 0;
|
|
--background-color: inherit;
|
|
--button-background-color: rgba(var(--text-color), 0.2);
|
|
--button-border-radius: 0.3rem;
|
|
}
|
|
.copy{
|
|
display: grid;
|
|
width: 100%;
|
|
gap: 0.5rem;
|
|
padding: var(--padding);
|
|
align-items: center;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
}
|
|
:host(:not([clip-text])) .copy-content{
|
|
overflow-wrap: break-word;
|
|
word-wrap: break-word;
|
|
}
|
|
:host([clip-text]) .copy-content{
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.copy-button{
|
|
display: inline-flex;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
border: none;
|
|
padding: 0.4rem;
|
|
background-color: inherit;
|
|
border-radius: var(--button-border-radius);
|
|
}
|
|
.copy-button:active{
|
|
background-color: var(--button-background-color);
|
|
}
|
|
.icon{
|
|
height: 1.2rem;
|
|
width: 1.2rem;
|
|
fill: rgba(var(--text-color), 0.8);
|
|
}
|
|
@media (any-hover: hover){
|
|
.copy:hover .copy-button{
|
|
opacity: 1;
|
|
}
|
|
.copy-button{
|
|
opacity: 0.6;
|
|
}
|
|
.copy-button:hover{
|
|
background-color: var(--button-background-color);
|
|
}
|
|
}
|
|
</style>
|
|
<section class="copy">
|
|
<p class="copy-content"></p>
|
|
<button part="button" class="copy-button" title="copy">
|
|
<slot name="copy-icon">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1.001 1.001 0 0 1 3 21l.003-14c0-.552.45-1 1.007-1H7zM5.003 8L5 20h10V8H5.003zM9 6h8v10h2V4H9v2z"/></svg>
|
|
</slot>
|
|
</button>
|
|
</section>
|
|
`;
|
|
customElements.define('sm-copy',
|
|
class extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({
|
|
mode: 'open'
|
|
}).append(smCopy.content.cloneNode(true));
|
|
|
|
this.copyContent = this.shadowRoot.querySelector('.copy-content');
|
|
this.copyButton = this.shadowRoot.querySelector('.copy-button');
|
|
|
|
this.copy = this.copy.bind(this);
|
|
}
|
|
static get observedAttributes() {
|
|
return ['value'];
|
|
}
|
|
set value(val) {
|
|
this.setAttribute('value', val);
|
|
}
|
|
get value() {
|
|
return this.getAttribute('value');
|
|
}
|
|
fireEvent() {
|
|
this.dispatchEvent(
|
|
new CustomEvent('copy', {
|
|
composed: true,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
})
|
|
);
|
|
}
|
|
copy() {
|
|
navigator.clipboard.writeText(this.copyContent.textContent)
|
|
.then(res => this.fireEvent())
|
|
.catch(err => console.error(err));
|
|
}
|
|
connectedCallback() {
|
|
this.copyButton.addEventListener('click', this.copy);
|
|
}
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (name === 'value') {
|
|
this.copyContent.textContent = newValue;
|
|
}
|
|
}
|
|
disconnectedCallback() {
|
|
this.copyButton.removeEventListener('click', this.copy);
|
|
}
|
|
});
|
|
const smForm = document.createElement('template');
|
|
smForm.innerHTML = `
|
|
<style>
|
|
*{
|
|
padding: 0;
|
|
margin: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
:host{
|
|
display: flex;
|
|
width: 100%;
|
|
}
|
|
form{
|
|
display: grid;
|
|
gap: var(--gap, 1.5rem);
|
|
width: 100%;
|
|
}
|
|
</style>
|
|
<form part="form" onsubmit="return false">
|
|
<slot></slot>
|
|
</form>
|
|
`;
|
|
|
|
customElements.define('sm-form', class extends HTMLElement {
|
|
constructor() {
|
|
super()
|
|
this.attachShadow({
|
|
mode: 'open'
|
|
}).append(smForm.content.cloneNode(true))
|
|
|
|
this.form = this.shadowRoot.querySelector('form');
|
|
this.formElements
|
|
this.requiredElements
|
|
this.submitButton
|
|
this.resetButton
|
|
this.allRequiredValid = false;
|
|
|
|
this.debounce = this.debounce.bind(this)
|
|
this._checkValidity = this._checkValidity.bind(this)
|
|
this.handleKeydown = this.handleKeydown.bind(this)
|
|
this.reset = this.reset.bind(this)
|
|
this.elementsChanged = this.elementsChanged.bind(this)
|
|
}
|
|
debounce(callback, wait) {
|
|
let timeoutId = null;
|
|
return (...args) => {
|
|
window.clearTimeout(timeoutId);
|
|
timeoutId = window.setTimeout(() => {
|
|
callback.apply(null, args);
|
|
}, wait);
|
|
};
|
|
}
|
|
_checkValidity() {
|
|
this.allRequiredValid = this.requiredElements.every(elem => elem.isValid)
|
|
if (!this.submitButton) return;
|
|
if (this.allRequiredValid) {
|
|
this.submitButton.disabled = false;
|
|
}
|
|
else {
|
|
this.submitButton.disabled = true;
|
|
}
|
|
}
|
|
handleKeydown(e) {
|
|
if (e.key === 'Enter' && e.target.tagName !== 'SM-TEXTAREA') {
|
|
if (this.allRequiredValid) {
|
|
if (this.submitButton && this.submitButton.tagName === 'SM-BUTTON') {
|
|
this.submitButton.click()
|
|
}
|
|
this.dispatchEvent(new CustomEvent('submit', {
|
|
bubbles: true,
|
|
composed: true,
|
|
}))
|
|
}
|
|
else {
|
|
this.requiredElements.find(elem => !elem.isValid).vibrate()
|
|
}
|
|
}
|
|
}
|
|
reset() {
|
|
this.formElements.forEach(elem => elem.reset())
|
|
}
|
|
elementsChanged() {
|
|
this.formElements = [...this.querySelectorAll('sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio')]
|
|
this.requiredElements = this.formElements.filter(elem => elem.hasAttribute('required'));
|
|
this.submitButton = this.querySelector('[variant="primary"], [type="submit"]');
|
|
this.resetButton = this.querySelector('[type="reset"]');
|
|
if (this.resetButton) {
|
|
this.resetButton.addEventListener('click', this.reset);
|
|
}
|
|
this._checkValidity()
|
|
}
|
|
connectedCallback() {
|
|
const slot = this.shadowRoot.querySelector('slot')
|
|
slot.addEventListener('slotchange', this.elementsChanged)
|
|
this.addEventListener('input', this.debounce(this._checkValidity, 100));
|
|
this.addEventListener('keydown', this.debounce(this.handleKeydown, 100));
|
|
}
|
|
disconnectedCallback() {
|
|
this.removeEventListener('input', this.debounce(this._checkValidity, 100));
|
|
this.removeEventListener('keydown', this.debounce(this.handleKeydown, 100));
|
|
}
|
|
})
|
|
|
|
</script>
|
|
</template>
|
|
</body>
|
|
|
|
</html> |