1533 lines
71 KiB
HTML
1533 lines
71 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>FLO LogSheet</title>
|
|
<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=Roboto:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
|
|
rel="stylesheet">
|
|
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
|
|
<script id="floGlobals">
|
|
/* Constants for FLO blockchain operations !!Make sure to add this at begining!! */
|
|
const floGlobals = {
|
|
|
|
//Required for all
|
|
blockchain: "FLO",
|
|
adminID: "FLf3jtkPQryh1ide7AJ8tigBmYzLj3h7Cq",
|
|
application: "logSheet",
|
|
// for storing single log sheet data
|
|
currentSheet: {}
|
|
}
|
|
</script>
|
|
<script src="scripts/lib.js" defer></script>
|
|
<script src="scripts/floCrypto.js" defer></script>
|
|
<script src="scripts/floBlockchainAPI.js" defer></script>
|
|
<script src="scripts/compactIDB.js" defer></script>
|
|
<script src="scripts/floCloudAPI.js" defer></script>
|
|
<script src="scripts/floDapps.js" defer></script>
|
|
<script src="scripts/btcOperator.js" defer></script>
|
|
<script src="scripts/logsheet.js" defer></script>
|
|
<script id="onLoadStartUp">
|
|
function onLoadStartUp() {
|
|
//display loading screen
|
|
showLoader()
|
|
//set the custom Privkey input
|
|
floDapps.setCustomPrivKeyInput(() => {
|
|
return new Promise((resolve, reject) => {
|
|
hideLoader()
|
|
showPage('sign_in_page')
|
|
getRef('sign_in_btn').onclick = () => {
|
|
closePopup()
|
|
showLoader()
|
|
resolve(getRef('get_priv_key_field').value.trim())
|
|
getRef('get_priv_key_field').value = ''
|
|
}
|
|
})
|
|
})
|
|
//invoke the startup functions
|
|
floDapps.launchStartUp().then(result => {
|
|
console.log(result)
|
|
let floId = floDapps.user.id
|
|
if (!floCrypto.validateFloID(floId)) {
|
|
const type = coinjs.addressDecode(floId).type
|
|
if (type === 'standard') {
|
|
floId = btcOperator.convert.legacy2legacy(floId, 0x23);
|
|
} else if (type === 'bech32') {
|
|
floId = btcOperator.convert.bech2legacy(floId, 0x23);
|
|
} else {
|
|
notify(`Multisig address can't be used to sign in`, 'error');
|
|
return;
|
|
}
|
|
}
|
|
floGlobals.myFloID = floId
|
|
floGlobals.myBtcID = btcOperator.convert.legacy2bech(floId)
|
|
getRef('user_profile_id').textContent = floGlobals.myFloID
|
|
let showingFloID = true
|
|
// alternating between floID and btcID every 10 seconds
|
|
setInterval(() => {
|
|
getRef('user_profile_id').textContent = showingFloID ? floGlobals.myBtcID : floGlobals.myFloID
|
|
showingFloID = !showingFloID
|
|
}, 10000)
|
|
//request object data from Supernode
|
|
logSheet.init().then(result => {
|
|
console.log(result)
|
|
floGlobals.isSubAdmin = floGlobals.subAdmins.includes(myFloID)
|
|
floGlobals.isTrustedID = floGlobals.trustedIDs.includes(myFloID)
|
|
hideLoader()
|
|
showPage(window.location.hash, { firstLoad: true })
|
|
//display add buttons if subAdmin, else hide
|
|
if (floGlobals.isSubAdmin) {
|
|
document.querySelectorAll('.sub-admin-option').forEach(option => option.classList.remove('hidden'))
|
|
} else {
|
|
document.querySelectorAll('.sub-admin-option').forEach(option => option.classList.add('hidden'))
|
|
}
|
|
}).catch(error => {
|
|
console.error(`Failed to download objectData`);
|
|
notify(error, "error")
|
|
})
|
|
}).catch(error => notify(error, "error"))
|
|
}
|
|
</script>
|
|
</head>
|
|
|
|
<body onload="onLoadStartUp()" class="hidden">
|
|
<sm-notifications id="notification_drawer"></sm-notifications>
|
|
<sm-popup id="confirmation_popup">
|
|
<h4 id="confirm_title"></h4>
|
|
<p id="confirm_message" class="breakable"></p>
|
|
<div class="flex align-center gap-0-5 margin-left-auto">
|
|
<button class="button cancel-button">Cancel</button>
|
|
<button class="button button--primary confirm-button">OK</button>
|
|
</div>
|
|
</sm-popup>
|
|
<!-- Loading screen-->
|
|
<section id="main_loader" class="grid">
|
|
<sm-spinner></sm-spinner>
|
|
<h4 id="tip_container">Loading RanchiMall FLO LogSheet</h4>
|
|
<sm-button onclick="signOut()">Sign Out</sm-button>
|
|
</section>
|
|
<div id="save_button" class="hidden flex align-center space-between" data-unsaved="0">
|
|
<span id="changes_indicator"></span>
|
|
<p>Changes need to be saved!</p>
|
|
<sm-button>Save</sm-button>
|
|
</div>
|
|
|
|
<main>
|
|
<div id="sign_in_page" class="page grid space-between">
|
|
<div class="info">
|
|
<h4>RanchiMall</h4>
|
|
<h1>LogSheet</h1>
|
|
<p>Open • Distributed • Reliable</p>
|
|
</div>
|
|
<div class="sign-in-box flex flex-direction-column gap-2">
|
|
<sm-chips id="entry_type_selector" class="align-self-start">
|
|
<sm-chip value="sign-in" selected>Sign in</sm-chip>
|
|
<sm-chip value="sign-up">Sign up</sm-chip>
|
|
</sm-chips>
|
|
<div id="user_entry">
|
|
<div>
|
|
<h3>Welcome back</h3>
|
|
<p>Just enter your FLO private key to continue.</p>
|
|
<sm-form>
|
|
<sm-input id="get_priv_key_field" data-privateKey placeholder="FLO private key"
|
|
type="password" required animate>
|
|
</sm-input>
|
|
<button id="sign_in_btn" class="button button--primary" type="submit" disabled>
|
|
Sign In
|
|
</button>
|
|
</sm-form>
|
|
</div>
|
|
<div class="hidden">
|
|
<h3>Get started</h3>
|
|
<p>Create your FLO public and private key pair. <Strong>Don't forget to store them
|
|
securely!</Strong></p>
|
|
<sm-button id="generate_flo_id" onclick="generateId()" variant="primary">Get FLO credentials
|
|
</sm-button>
|
|
<section id="credentials_section" class="hidden grid gap-1">
|
|
<div class="grid gap-0-3">
|
|
<h5>FLO ID</h5>
|
|
<sm-copy id="generated_id"></sm-copy>
|
|
</div>
|
|
<div class="grid gap-0-3">
|
|
<h5>Private key</h5>
|
|
<sm-copy id="generated_key"></sm-copy>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<header id="main_header" class="grid">
|
|
<div class="flex flex-direction-column hide-on-mobile">
|
|
<h5>RanchiMall</h5>
|
|
<h4>FLO LogSheet</h4>
|
|
</div>
|
|
<theme-toggle class="margin-left-auto"></theme-toggle>
|
|
<button id="user_profile_button" class="user-content button--small" onclick="openPopup('profile_popup')">
|
|
<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>
|
|
<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">
|
|
</path>
|
|
</svg>
|
|
<div id="user_profile_id" class="overflow-ellipsis"></div>
|
|
</button>
|
|
</header>
|
|
<section id="home_page" class="page grid hidden">
|
|
<section id="main_section">
|
|
<header class="flex align-center space-between section-header">
|
|
<h4>Sheets</h4>
|
|
<sm-input id="search_sheets" placeholder="Search sheets" type="search">
|
|
<svg class="icon" slot="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.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>
|
|
</header>
|
|
<section id="sheets_container" class="grid"></section>
|
|
</section>
|
|
</section>
|
|
<section id="sheet_view" class="page grid hidden">
|
|
<nav id="side_bar">
|
|
<div class="flex align-center">
|
|
<button class="hide-on-desktop" onclick="toggleSideBar()">
|
|
<svg class="icon margin-right-0-3" 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>
|
|
<h4 class="section-header">People</h4>
|
|
<button class="button button--small margin-left-auto sub-admin-option"
|
|
onclick="openPopup('add_person_popup')">Add
|
|
person</button>
|
|
</div>
|
|
<div id="people_container" class="grid"></div>
|
|
</nav>
|
|
<section id="right">
|
|
<section id="sheet_details">
|
|
<div class="flex align-center">
|
|
<a href="#/home_page" class="flex align-center">
|
|
<svg class="icon margin-right-0-3" 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>
|
|
Sheets
|
|
</a>
|
|
<div id="sheet_processing_options" class="flex gap-0-5 margin-left-auto hidden">
|
|
<button class="button button--small" onclick="initSheetProcessing('group')">Group
|
|
by</button>
|
|
<button class="button button--small" onclick="initSheetProcessing('aggregate')">Aggregate
|
|
by</button>
|
|
</div>
|
|
</div>
|
|
<div class="flex align-center">
|
|
<button onclick="toggleSideBar()">
|
|
<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="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
|
|
</svg>
|
|
</button>
|
|
<h3 id="sheet_heading"></h3>
|
|
<h5 id="sheet_type"></h5>
|
|
<button id="toggle_details" class="margin-left-auto" onclick="toggleDetails()">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
|
width="24px" fill="#000000">
|
|
<path d="M24 24H0V0h24v24z" fill="none" opacity=".87" />
|
|
<path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div id="sheet_editors" class="flex align-center flex-wrap"></div>
|
|
<p id="sheet_description"></p>
|
|
</section>
|
|
<div id="sheet_container">
|
|
<table>
|
|
<thead id="sheet_container__header"></thead>
|
|
<tbody id="sheet_container__body"></tbody>
|
|
</table>
|
|
</div>
|
|
<form id="new-log" style="display: none;" onsubmit="enterLog(event); return false;"></form>
|
|
<button type="submit" form="new-log" style="display: none;"></button>
|
|
</section>
|
|
</section>
|
|
</main>
|
|
<!-- User settings popup -->
|
|
<sm-popup id="profile_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close justify-self-start" 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>Profile</h3>
|
|
</header>
|
|
<div id="profile_popup__content" class="grid gap-3"></div>
|
|
</sm-popup>
|
|
|
|
<!-- Add a person popup -->
|
|
<sm-popup id="add_person_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<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="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Add new person</h3>
|
|
</header>
|
|
<sm-form>
|
|
<div class="grid gap-0-5">
|
|
<sm-input id="person_flo_id_input" floId placeholder="FLO ID*" animate autofocus required></sm-input>
|
|
<sm-input id="person_name_input" placeholder="Name*" animate required></sm-input>
|
|
</div>
|
|
<div id="specify_details" class="grid gap-0-5">
|
|
<div class="flex flex-direction-column gap-0-3">
|
|
<h4>Additional details</h4>
|
|
<p>Add more details about a person. Specify type of information and actual detail.</p>
|
|
</div>
|
|
<div id="additional_fields" class="grid"></div>
|
|
<div id="add_detail" class="grid align-center">
|
|
<sm-input id="add_detail_type_input" exclude placeholder="Type"></sm-input>
|
|
<sm-input id="add_detail_value_input" exclude placeholder="Value"></sm-input>
|
|
<button class="button margin-left-auto" onclick="addDetails()">
|
|
<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="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
|
|
</svg>
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button onclick="addPerson()" class="button button--primary" type="submit" disabled>
|
|
Add person
|
|
</button>
|
|
</sm-form>
|
|
</sm-popup>
|
|
<!-- Create new sheet popup -->
|
|
<sm-popup id="new_sheet_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<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="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" />
|
|
</svg>
|
|
</button>
|
|
<h3>Create new sheet</h3>
|
|
</header>
|
|
<sm-form>
|
|
<sm-input id="sheet_title_input" placeholder="Name*" autofocus animate required></sm-input>
|
|
<sm-textarea id="sheet_description_input" placeholder="Description" rows="4" animate required></sm-textarea>
|
|
<div id="specify_columns" class="grid gap-0-5">
|
|
<div class="flex flex-direction-column">
|
|
<h4>Add Columns</h4>
|
|
<p>Columns will be added as the order you added them.</p>
|
|
</div>
|
|
<div id="columns_container" class="flex flex-wrap"></div>
|
|
<div id="add_column" class="flex align-center space-between">
|
|
<sm-input id="add_column_input" exclude placeholder="Column title"></sm-input>
|
|
<button class="button" onclick="addColumn()">
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<sm-switch id="sheet_type_switch" onchange="toggleEditors()">
|
|
<div slot="left" class="flex align-center space-between">
|
|
Make sheet private
|
|
</div>
|
|
</sm-switch>
|
|
<div id="specify_editors" class="grid gap-0-5 hidden">
|
|
<div class="flex flex-direction-column">
|
|
<h4>Add editors</h4>
|
|
<p>Only specified editors will be able to update this sheet.</p>
|
|
</div>
|
|
<div id="editors_container" class="grid gap-0-3"></div>
|
|
<div id="add_editor" class="flex align-center space-between">
|
|
<sm-input id="add_editor_input" exclude placeholder="Editor's FLO Address"></sm-input>
|
|
<button class="button" onclick="addEditor()">
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button onclick="createSheet()" class="button button--primary" type="submit" disabled>
|
|
Create
|
|
</button>
|
|
</sm-form>
|
|
</sm-popup>
|
|
|
|
<!-- Group by popup -->
|
|
<sm-popup id="sheet_processing_popup">
|
|
<header slot="header" class="popup__header">
|
|
<button class="popup__header__close" onclick="closePopup()">
|
|
<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="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" />
|
|
</svg>
|
|
</button>
|
|
<h3 id="sheet_processing_title"> </h3>
|
|
</header>
|
|
<div class="flex align-center" id="group_by_menu">
|
|
<sm-select id="process_by_method_selector">
|
|
<sm-option value="count">Count</sm-option>
|
|
<sm-option value="total" selected>Total</sm-option>
|
|
<sm-option value="avg">Avg</sm-option>
|
|
<sm-option value="max">Max</sm-option>
|
|
<sm-option value="min">Min</sm-option>
|
|
</sm-select>
|
|
<sm-select id="process_by_attribute_selector"></sm-select>
|
|
</div>
|
|
<div id="processed_sheet_view" class="grid"></div>
|
|
</sm-popup>
|
|
<script src="components.js"></script>
|
|
<script>
|
|
/*jshint esversion: 8 */
|
|
/**
|
|
* @yaireo/relative-time - javascript function to transform timestamp or date to local relative-time
|
|
*
|
|
* @version v1.0.0
|
|
* @homepage https://github.com/yairEO/relative-time
|
|
*/
|
|
|
|
!function (e, t) { var o = o || {}; "function" == typeof o && o.amd ? o([], t) : "object" == typeof exports && "object" == typeof module ? module.exports = t() : "object" == typeof exports ? exports.RelativeTime = t() : e.RelativeTime = t() }(this, (function () { const e = { year: 31536e6, month: 2628e6, day: 864e5, hour: 36e5, minute: 6e4, second: 1e3 }, t = "en", o = { numeric: "auto" }; function n(e) { e = { locale: (e = e || {}).locale || t, options: { ...o, ...e.options } }, this.rtf = new Intl.RelativeTimeFormat(e.locale, e.options) } return n.prototype = { from(t, o) { const n = t - (o || new Date); for (let t in e) if (Math.abs(n) > e[t] || "second" == t) return this.rtf.format(Math.round(n / e[t]), t) } }, n }));
|
|
|
|
const relativeTime = new RelativeTime({ style: 'narrow' });
|
|
</script>
|
|
<script id="default_ui_library">
|
|
// Global variables
|
|
const { html, render: renderElem } = uhtml;
|
|
const domRefs = {}
|
|
let frag = document.createDocumentFragment();
|
|
//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);
|
|
};
|
|
}
|
|
let zIndex = 50
|
|
// function required for popups or modals to appear
|
|
function openPopup(popupId, pinned) {
|
|
zIndex++
|
|
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
|
|
return getRef(popupId).show({ pinned })
|
|
}
|
|
|
|
// hides the popup or modal
|
|
function closePopup(options = {}) {
|
|
if (popupStack.peek() === undefined)
|
|
return;
|
|
popupStack.peek().popup.hide(options)
|
|
}
|
|
|
|
|
|
// 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', danger = false } = options
|
|
getRef('confirm_title').innerText = title;
|
|
getRef('confirm_message').innerText = message;
|
|
const cancelButton = getRef('confirmation_popup').querySelector('.cancel-button');
|
|
const confirmButton = getRef('confirmation_popup').querySelector('.confirm-button')
|
|
confirmButton.textContent = confirmText
|
|
cancelButton.textContent = cancelText
|
|
if (danger)
|
|
confirmButton.classList.add('button--danger')
|
|
else
|
|
confirmButton.classList.remove('button--danger')
|
|
const { opened, closed } = openPopup('confirmation_popup')
|
|
confirmButton.onclick = () => {
|
|
closePopup({ payload: true })
|
|
}
|
|
cancelButton.onclick = () => {
|
|
closePopup()
|
|
}
|
|
closed.then((payload) => {
|
|
confirmButton.onclick = null
|
|
cancelButton.onclick = null
|
|
if (payload)
|
|
resolve(true)
|
|
else
|
|
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>`
|
|
options.pinned = true
|
|
break;
|
|
}
|
|
getRef("notification_drawer").push(message, { icon, ...options });
|
|
if (mode === 'error') {
|
|
console.error(message)
|
|
}
|
|
}
|
|
|
|
// detect browser version
|
|
function detectBrowser() {
|
|
let ua = navigator.userAgent,
|
|
tem,
|
|
M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
|
|
if (/trident/i.test(M[1])) {
|
|
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
|
|
return 'IE ' + (tem[1] || '');
|
|
}
|
|
if (M[1] === 'Chrome') {
|
|
tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
|
|
if (tem != null) return tem.slice(1).join(' ').replace('OPR', 'Opera');
|
|
}
|
|
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
|
|
if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1]);
|
|
return M.join(' ');
|
|
}
|
|
window.addEventListener('hashchange', e => showPage(window.location.hash))
|
|
window.addEventListener("load", () => {
|
|
const [browserName, browserVersion] = detectBrowser().split(' ');
|
|
const supportedVersions = {
|
|
Chrome: 85,
|
|
Firefox: 75,
|
|
Safari: 13,
|
|
}
|
|
if (browserName in supportedVersions) {
|
|
if (parseInt(browserVersion) < supportedVersions[browserName]) {
|
|
notify(`${browserName} ${browserVersion} is not fully supported, some features may not work properly. Please update to ${supportedVersions[browserName]} or higher.`, 'error')
|
|
}
|
|
} else {
|
|
notify('Browser is not fully compatible, some features may not work. for best experience please use Chrome, Edge, Firefox or Safari', 'error')
|
|
}
|
|
document.body.classList.remove('hidden')
|
|
document.querySelectorAll('sm-input[data-flo-id]').forEach(input => input.customValidation = floCrypto.validateAddr)
|
|
document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex)
|
|
document.addEventListener('keyup', (e) => {
|
|
if (e.code === 'Escape') {
|
|
closePopup()
|
|
}
|
|
})
|
|
document.addEventListener("pointerdown", (e) => {
|
|
if (e.target.closest("button:not([disabled]), sm-button:not([disabled]), .interactive")) {
|
|
createRipple(e, e.target.closest("button, sm-button, .interactive"));
|
|
}
|
|
});
|
|
document.addEventListener('copy', () => {
|
|
notify('copied', 'success')
|
|
})
|
|
});
|
|
|
|
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();
|
|
};
|
|
}
|
|
|
|
function getFormattedTime(timestamp, format) {
|
|
try {
|
|
timestamp = parseInt(timestamp)
|
|
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;
|
|
}
|
|
}
|
|
function buttonLoader(id, show) {
|
|
const button = typeof id === 'string' ? getRef(id) : id;
|
|
button.disabled = show;
|
|
const animOptions = {
|
|
duration: 200,
|
|
fill: 'forwards',
|
|
easing: 'ease'
|
|
}
|
|
if (show) {
|
|
button.animate([
|
|
{
|
|
clipPath: 'circle(100%)',
|
|
},
|
|
{
|
|
clipPath: 'circle(0)',
|
|
},
|
|
], animOptions).onfinish = e => {
|
|
e.target.commitStyles()
|
|
e.target.cancel()
|
|
}
|
|
button.parentNode.append(createElement('sm-spinner'))
|
|
} else {
|
|
button.style = ''
|
|
const potentialTarget = button.parentNode.querySelector('sm-spinner')
|
|
if (potentialTarget) potentialTarget.remove();
|
|
}
|
|
}
|
|
// implement event delegation
|
|
function delegate(el, event, selector, fn) {
|
|
el.addEventListener(event, function (e) {
|
|
const potentialTarget = e.target.closest(selector)
|
|
if (potentialTarget) {
|
|
e.delegateTarget = potentialTarget
|
|
fn.call(this, e)
|
|
}
|
|
})
|
|
}
|
|
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)'
|
|
},
|
|
]
|
|
function showChildElement(id, index, options = {}) {
|
|
return new Promise((resolve) => {
|
|
const { mobileView = false, entry, exit } = options
|
|
const animOptions = {
|
|
duration: 150,
|
|
easing: 'ease',
|
|
fill: 'forwards'
|
|
}
|
|
const visibleElement = [...getRef(id).children].find(elem => !elem.classList.contains(mobileView ? 'hide-on-mobile' : 'hidden'));
|
|
if (visibleElement === getRef(id).children[index]) return;
|
|
visibleElement.getAnimations().forEach(anim => anim.cancel())
|
|
getRef(id).children[index].getAnimations().forEach(anim => anim.cancel())
|
|
if (visibleElement) {
|
|
if (exit) {
|
|
visibleElement.animate(exit, animOptions).onfinish = () => {
|
|
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden')
|
|
getRef(id).children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
|
|
if (entry)
|
|
getRef(id).children[index].animate(entry, animOptions).onfinish = () => resolve()
|
|
}
|
|
} else {
|
|
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden')
|
|
getRef(id).children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
|
|
resolve()
|
|
}
|
|
} else {
|
|
getRef(id).children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
|
|
getRef(id).children[index].animate(entry, animOptions).onfinish = () => resolve()
|
|
}
|
|
})
|
|
}
|
|
|
|
document.addEventListener('popupopened', e => {
|
|
switch (e.target.id) {
|
|
case 'sheet_processing_popup':
|
|
renderProcessedSheet()
|
|
break;
|
|
case 'profile_popup':
|
|
renderElem(getRef('profile_popup__content'), render.profile())
|
|
break;
|
|
}
|
|
})
|
|
document.addEventListener('popupclosed', e => {
|
|
switch (e.target.id) {
|
|
case 'new_sheet_popup':
|
|
getRef('columns_container').innerHTML = ''
|
|
getRef('editors_container').innerHTML = ''
|
|
break;
|
|
case 'sheet_processing_popup':
|
|
renderElem(getRef("processed_sheet_view"), html``)
|
|
break;
|
|
}
|
|
})
|
|
function showLoader() {
|
|
document.body.classList.remove('hidden')
|
|
getRef('main_loader').classList.remove('hidden')
|
|
document.querySelector('main').classList.add('hidden')
|
|
getRef('main_header').classList.add('hidden')
|
|
}
|
|
function hideLoader() {
|
|
getRef('main_loader').classList.add('hidden')
|
|
document.querySelector('main').classList.remove('hidden')
|
|
getRef('main_header').classList.remove('hidden')
|
|
}
|
|
|
|
|
|
function setAttributes(el, attrs) {
|
|
for (var key in attrs) {
|
|
el.setAttribute(key, attrs[key]);
|
|
}
|
|
}
|
|
delegate(getRef('people_container'), 'click', '.person-card', e => {
|
|
copyToLogInput(e.delegateTarget.dataset.floId)
|
|
})
|
|
let currentTableHeader
|
|
delegate(getRef('sheet_container__header'), 'click', 'th', e => {
|
|
if (currentTableHeader && currentTableHeader !== e.delegateTarget)
|
|
currentTableHeader.classList.remove('ascending', 'descending')
|
|
sortTable(e.delegateTarget.cellIndex, e.delegateTarget.classList.contains('descending'))
|
|
if (e.delegateTarget.classList.contains('descending'))
|
|
e.delegateTarget.classList.add('ascending')
|
|
else
|
|
e.delegateTarget.classList.remove('ascending')
|
|
e.delegateTarget.classList.toggle('descending')
|
|
currentTableHeader = e.delegateTarget
|
|
})
|
|
const intersectionObserver = new IntersectionObserver((entries, observer) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
renderSheetView({ ...floGlobals.currentSheet, onlyRenderTable: true, lazyLoad: true })
|
|
observer.disconnect()
|
|
}
|
|
})
|
|
}, {
|
|
root: getRef('sheet_container'),
|
|
})
|
|
|
|
const watchLastElement = new MutationObserver(mutations => {
|
|
mutations.forEach(mutation => {
|
|
if (mutation.addedNodes.length) {
|
|
intersectionObserver.observe(mutation.addedNodes[mutation.addedNodes.length - 1])
|
|
}
|
|
})
|
|
})
|
|
watchLastElement.observe(getRef('sheet_container__body'), { childList: true, subtree: true })
|
|
|
|
getRef('search_sheets').addEventListener('input', function () {
|
|
if (this.value.trim !== '') {
|
|
for (child of getRef('sheets_container').children) {
|
|
if (child.id === 'add_new_sheet') continue
|
|
if (child.dataset.name.toLowerCase().includes(this.value.trim().toLowerCase())) {
|
|
child.classList.remove('hidden')
|
|
} else {
|
|
child.classList.add('hidden')
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
const render = {
|
|
sheetCard(sheetId, details) {
|
|
const { title, editors } = details
|
|
const type = editors ? 'Private' : 'Public'
|
|
return html`
|
|
<a class="sheet-card interactive" href=${`#/sheet_view?id=${sheetId}`} .dataset=${{ name: title }}>
|
|
<div class="sheet-card__icon">
|
|
<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="M8 16h8v2H8zm0-4h8v2H8zm6-10H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/></svg>
|
|
</div>
|
|
<h4>${title}</h4>
|
|
<span class="sheet-card__type">${type}</span>
|
|
</a>
|
|
`;
|
|
},
|
|
personCard(floId, details, ref) {
|
|
const { name } = details
|
|
return html.for(ref, floId)`
|
|
<div class="person-card interactive" .dataset=${{ floId, name }}>
|
|
<h3 class="person-initials">${name.charAt(0)}</h3>
|
|
<h4 class="person-name">${name}</h4>
|
|
<h5 class="person-flo-id overflow-ellipsis">${floId}</h5>
|
|
</div>
|
|
`;
|
|
},
|
|
editorCard(floId) {
|
|
let card = document.createElement('div')
|
|
card.classList.add('editor-card', 'flex', 'space-between', 'align-center')
|
|
setAttributes(card, {
|
|
'data-flo-id': floId
|
|
})
|
|
card.innerHTML = `
|
|
<h4 class="editor-address wrap-around">${floId}</h4>
|
|
<button onclick="this.parentNode.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 d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-3.5l-1-1zM18 7H6v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7z"/></svg>
|
|
</button>
|
|
`
|
|
return card
|
|
},
|
|
columnCard(title) {
|
|
let card = document.createElement('div')
|
|
card.classList.add('column-card', 'flex', 'space-between', 'align-center')
|
|
setAttributes(card, {
|
|
'data-column-title': title
|
|
})
|
|
card.innerHTML = `
|
|
<h5 class="column-title">${title}</h5>
|
|
<button onclick="this.parentNode.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 d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-3.5l-1-1zM18 7H6v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7z"/></svg>
|
|
</button>
|
|
`
|
|
return card
|
|
},
|
|
detailsCard(type, value) {
|
|
let card = document.createElement('div')
|
|
card.classList.add('details-card', 'grid', 'align-center')
|
|
setAttributes(card, {
|
|
'data-type': type,
|
|
'data-value': value
|
|
})
|
|
card.innerHTML = `
|
|
<h5 class="type">${type}</h5>
|
|
<h4 class="value">${value}</h4>
|
|
<button onclick="this.parentNode.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 d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-3.5l-1-1zM18 7H6v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7z"/></svg>
|
|
</button>
|
|
`
|
|
return card
|
|
},
|
|
profile() {
|
|
return html`
|
|
<div class="grid gap-1-5">
|
|
<div class="grid gap-0-5">
|
|
<h4>
|
|
BTC integrated with FLO
|
|
</h4>
|
|
<p>
|
|
You can use your FLO private key to perform transactions on the BTC network within our
|
|
app
|
|
ecosystem. The private key is the same for both.
|
|
</p>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<b>My FLO address</b>
|
|
<sm-copy class="user-flo-id" clip-text value=${floGlobals.myFloID}></sm-copy>
|
|
</div>
|
|
<div class="grid gap-0-5">
|
|
<b>My Bitcoin address</b>
|
|
<sm-copy class="user-btc-id" clip-text value=${floGlobals.myBtcID}></sm-copy>
|
|
</div>
|
|
<button class="button button--danger justify-self-start" onclick="signOut()">Sign out</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function addEditor() {
|
|
let address = getRef('add_editor_input').value.trim()
|
|
if (address === '') return;
|
|
if (floCrypto.validateAddr(address)) {
|
|
getRef('editors_container').append(render.editorCard(address))
|
|
getRef('add_editor_input').value = ''
|
|
} else {
|
|
notify('Invalid editor FLO address', 'error')
|
|
}
|
|
}
|
|
function addColumn() {
|
|
let columnTitle = getRef('add_column_input').value.trim()
|
|
if (columnTitle !== '') {
|
|
getRef('columns_container').append(render.columnCard(columnTitle))
|
|
getRef('add_column_input').value = ''
|
|
getRef('add_column_input').focusIn()
|
|
} else
|
|
notify("Column name should be specified.", 'error')
|
|
}
|
|
|
|
function addDetails() {
|
|
let type = getRef('add_detail_type_input').value.trim()
|
|
let value = getRef('add_detail_value_input').value.trim()
|
|
if (type !== '' && value !== '') {
|
|
getRef('additional_fields').append(render.detailsCard(type, value))
|
|
getRef('add_detail_type_input').value = ''
|
|
getRef('add_detail_value_input').value = ''
|
|
getRef('add_detail_type_input').focusIn()
|
|
}
|
|
else
|
|
notify('Please enter both type and value', 'error')
|
|
}
|
|
|
|
function toggleDetails() {
|
|
getRef('sheet_details').classList.toggle('collapse')
|
|
}
|
|
function toggleSideBar() {
|
|
getRef('sheet_view').classList.toggle('toggle-side-bar')
|
|
}
|
|
function toggleEditors() {
|
|
getRef('specify_editors').classList.toggle('hidden')
|
|
}
|
|
let lastPage
|
|
function showPage(targetPage, options = {}) {
|
|
const { firstLoad, hashChange } = options
|
|
let pageId
|
|
let searchParams
|
|
let params
|
|
if (targetPage === '') {
|
|
pageId = 'home_page'
|
|
}
|
|
else {
|
|
if (targetPage.includes('/')) {
|
|
let path;
|
|
[path, searchParams] = targetPage.split('?');
|
|
[, pageId] = path.split('/')
|
|
} else {
|
|
pageId = targetPage
|
|
}
|
|
}
|
|
|
|
if (!document.querySelector(`#${pageId}.page`)) return
|
|
|
|
if (searchParams) {
|
|
const urlSearchParams = new URLSearchParams('?' + searchParams);
|
|
params = Object.fromEntries(urlSearchParams.entries());
|
|
}
|
|
switch (pageId) {
|
|
case 'sign_in_page':
|
|
getRef('get_priv_key_field').focusIn();
|
|
break;
|
|
case 'home_page':
|
|
getRef('sheet_container__body').innerHTML = ''
|
|
getRef('sheet_heading').textContent = ''
|
|
getRef('sheet_type').textContent = ''
|
|
getRef('sheet_description').textContent = ''
|
|
getRef('main_header').classList.remove('hidden')
|
|
renderSheetList()
|
|
break;
|
|
case 'sheet_view':
|
|
renderPersonList()
|
|
if (params) {
|
|
viewSheet(params.id)
|
|
}
|
|
break;
|
|
}
|
|
if (pageId !== 'home_page') {
|
|
getRef('main_header').classList.add('hidden')
|
|
}
|
|
if (document.querySelector('.nav-list__item--active'))
|
|
document.querySelector('.nav-list__item--active').classList.remove('nav-list__item--active');
|
|
const targetListItem = [...document.querySelectorAll(`a.nav-list__item`)].find(item => item.href.includes(pageId))
|
|
if (targetListItem)
|
|
targetListItem.classList.add('nav-list__item--active')
|
|
if (pageId !== lastPage && document.querySelector('.page:not(.hidden)')) {
|
|
document.querySelector('.page:not(.hidden)').classList.add('hidden')
|
|
}
|
|
getRef(pageId).classList.remove('hidden')
|
|
lastPage = pageId
|
|
}
|
|
|
|
function generateId() {
|
|
getRef('generate_flo_id').classList.add('hidden')
|
|
getRef('credentials_section').classList.remove('hidden')
|
|
let { floID, privKey } = floCrypto.generateNewID()
|
|
getRef('generated_id').value = floID
|
|
getRef('generated_key').value = privKey
|
|
}
|
|
|
|
function signOut() {
|
|
getConfirmation('Sign out?', { message: 'You are about to sign out of the app, continue?', confirmText: 'Leave', cancelText: 'Stay' })
|
|
.then(async (res) => {
|
|
if (res) {
|
|
await floDapps.clearCredentials();
|
|
location.reload();
|
|
}
|
|
});
|
|
}
|
|
|
|
getRef('entry_type_selector').addEventListener('change', e => {
|
|
if (e.target.value === 'sign-in') {
|
|
showChildElement('user_entry', 0, { entry: slideInRight, exit: slideOutRight }).then(() => {
|
|
getRef('get_priv_key_field').focusIn()
|
|
})
|
|
} else {
|
|
showChildElement('user_entry', 1, { entry: slideInLeft, exit: slideOutLeft }).then(() => {
|
|
})
|
|
}
|
|
})
|
|
|
|
getRef('save_button').children[2].addEventListener("click", function (e) {
|
|
logSheet.commitUpdates().then(result => {
|
|
getRef('save_button').animate([
|
|
{
|
|
transform: 'none',
|
|
opacity: 1
|
|
},
|
|
{
|
|
transform: 'translateY(1rem)',
|
|
opacity: 0
|
|
},
|
|
], {
|
|
duration: 300,
|
|
easing: 'ease',
|
|
fill: 'forwards'
|
|
}).onfinish = e => {
|
|
getRef('save_button').classList.add('hidden')
|
|
getRef('save_button').dataset["unsaved"] = '0'
|
|
notify("Save Successful", 'success')
|
|
}
|
|
}).catch(error => notify(`Save Unsuccessful [${error}]`, error))
|
|
})
|
|
function initSheetProcessing(type) {
|
|
floGlobals.sheetProcessingMode = type
|
|
switch (type) {
|
|
case 'group':
|
|
getRef('sheet_processing_title').textContent = `Group ${floGlobals.currentSheet.title} by`
|
|
break;
|
|
case 'aggregate':
|
|
getRef('sheet_processing_title').textContent = `Aggregate ${floGlobals.currentSheet.title} by`
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
getRef('sheet_processing_popup').dataset.mode = type
|
|
openPopup('sheet_processing_popup')
|
|
}
|
|
|
|
getRef('process_by_method_selector').addEventListener("change", renderProcessedSheet)
|
|
|
|
getRef('process_by_attribute_selector').addEventListener("change", renderProcessedSheet)
|
|
|
|
function renderProcessedSheet() {
|
|
try {
|
|
const type = getRef('process_by_method_selector').value
|
|
const column = getRef('process_by_attribute_selector').value
|
|
const { sheetId, sheet } = floGlobals.currentSheet
|
|
switch (floGlobals.sheetProcessingMode) {
|
|
case 'group':
|
|
if (type === 'count') {
|
|
getRef('process_by_attribute_selector').classList.add("hidden")
|
|
let data = logSheet.groupBy.count(sheetId, sheet)
|
|
renderGroupByView("count", data)
|
|
} else {
|
|
getRef('process_by_attribute_selector').classList.remove("hidden")
|
|
let data = logSheet.groupBy[type](sheetId, sheet, column);
|
|
renderGroupByView(`${type}(${column})`, data)
|
|
}
|
|
break;
|
|
case 'aggregate':
|
|
let data = logSheet.aggBy[type](sheetId, sheet, column);
|
|
if (type === 'count') {
|
|
getRef('process_by_attribute_selector').classList.add("hidden")
|
|
} else {
|
|
getRef('process_by_attribute_selector').classList.remove("hidden")
|
|
}
|
|
if (data) {
|
|
if (type === 'count') {
|
|
renderElem(getRef('processed_sheet_view'), html`
|
|
<div class="grid gap-0-5 text-center" style="padding: 3rem 0;">
|
|
<h1>${data}</h1>
|
|
Total entries
|
|
</div>
|
|
`)
|
|
} else {
|
|
renderElem(getRef('processed_sheet_view'), html`
|
|
<div class="grid gap-0-5 text-center" style="padding: 3rem 0;">
|
|
<h1>${data}</h1>
|
|
<div class="capitalize">${type} ${column}</div>
|
|
</div>
|
|
`)
|
|
}
|
|
} else {
|
|
renderElem(getRef('processed_sheet_view'), html`
|
|
<h4 style="padding: 1.5rem 0;">${column} doesn't have numeric values</h4>
|
|
`)
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
notify(error, "error")
|
|
}
|
|
}
|
|
|
|
function showSave() {
|
|
getRef('save_button').classList.remove('hidden')
|
|
getRef('save_button').dataset["unsaved"] = parseInt(getRef('save_button').dataset["unsaved"]) + 1
|
|
getRef('save_button').animate([
|
|
{
|
|
transform: 'translateY(1rem)',
|
|
opacity: 0
|
|
},
|
|
{
|
|
transform: 'none',
|
|
opacity: 1
|
|
}
|
|
], {
|
|
duration: 300,
|
|
easing: 'ease',
|
|
fill: 'forwards'
|
|
}).onfinish = e => {
|
|
getRef('save_button').children[0].style.background = `#F${Math.max(9 - parseInt(getRef('save_button').dataset["unsaved"]), 0)}0`;
|
|
}
|
|
}
|
|
|
|
function addPerson() {
|
|
try {
|
|
let floID = getRef('person_flo_id_input').value.trim();
|
|
let name = getRef('person_name_input').value.trim();
|
|
let otherDetails = {}
|
|
let fields = getRef('additional_fields').querySelectorAll('.type, .value')
|
|
for (let i = 0; i < fields.length; i += 2)
|
|
otherDetails[fields[i].textContent] = fields[i + 1].textContent
|
|
console.log(fields, otherDetails)
|
|
logSheet.addPerson(floID, name, otherDetails)
|
|
renderPersonList()
|
|
closePopup()
|
|
notify(`Added person: ${floID}`, 'success')
|
|
showSave()
|
|
} catch (error) {
|
|
notify(error, "error")
|
|
}
|
|
}
|
|
|
|
function createSheet() {
|
|
try {
|
|
let title = getRef('sheet_title_input').value.trim();
|
|
let description = getRef('sheet_description_input').value.trim();
|
|
let columns = []
|
|
getRef('columns_container').querySelectorAll('.column-title').forEach(column => columns.push(column.textContent))
|
|
if (!columns.length) {
|
|
notify('Every LogSheet should contain at least 1 column.', 'error')
|
|
return
|
|
}
|
|
let editors = [...new Set([...getRef('editors_container').querySelectorAll('.editor-address')].map(editor => editor.textContent))]
|
|
if (!getRef('sheet_type_switch').checked)
|
|
editors = null;
|
|
else
|
|
if (!editors.length)
|
|
editors = floGlobals.subAdmins;
|
|
let sheetId = logSheet.createNewSheet(title, description, columns, editors)
|
|
let sheet = {}
|
|
sheet[sheetId] = { editors }
|
|
renderSheetList()
|
|
closePopup()
|
|
notify(`Created New Sheet: ${sheetId} (${title})`, 'success')
|
|
showSave()
|
|
} catch (error) {
|
|
notify(error, "error")
|
|
}
|
|
}
|
|
|
|
function copyToLogInput(floID) {
|
|
let input = document.forms['new-log'][0]
|
|
if (input.tagName === 'INPUT')
|
|
input.value = floID;
|
|
}
|
|
|
|
function enterLog(e) {
|
|
e.preventDefault()
|
|
let sheetId = floGlobals.currentSheet.sheetId;
|
|
let form = document.forms['new-log'],
|
|
allFormElements = document.querySelectorAll('input[form="new-log"]')
|
|
if (form[0].tagName === 'INPUT') {
|
|
let floID = form[0].value.trim();
|
|
if (floID === '' || !floCrypto.validateAddr(floID)) {
|
|
notify('Please enter a valid FLO ID', 'error')
|
|
form[0].focus()
|
|
return
|
|
}
|
|
let log = []
|
|
let isValid = false;
|
|
for (let i = 1; i < form.length - 1; i++) {
|
|
const value = form[i].value.trim()
|
|
log.push(value)
|
|
if (value !== '')
|
|
isValid = true
|
|
}
|
|
if (!isValid) {
|
|
form[1].focus()
|
|
return notify('Please enter value in at least one column', 'error')
|
|
}
|
|
allFormElements.forEach(element => element.disabled = true)
|
|
logSheet.enterLog(sheetId, floID, log).then(result => {
|
|
form.reset();
|
|
notify('Log entry successful', 'success')
|
|
getRef('sheet_container__body').prepend(html.node`
|
|
<tr>
|
|
<td>${floID}</td>
|
|
<td><input type="text" class="grade-input" disabled></td>
|
|
${log.map(l => html`<td>${l}</td>`)}
|
|
</tr>
|
|
`)
|
|
}).catch(error => {
|
|
notify(error, "error")
|
|
}).finally(() => {
|
|
allFormElements.forEach(element => element.disabled = false)
|
|
})
|
|
}
|
|
}
|
|
|
|
function viewSheet(sheetId) {
|
|
getRef('sheet_container').classList.add('placeholder')
|
|
getRef('sheet_heading').classList.add('placeholder')
|
|
getRef('sheet_type').classList.add('placeholder')
|
|
getRef('sheet_description').classList.add('placeholder')
|
|
getRef('sheet_container__header').classList.add('hidden')
|
|
getRef('sheet_processing_options').classList.add('hidden')
|
|
showPage('sheet_view')
|
|
logSheet.refreshLogs(sheetId).then(result => {
|
|
getRef('sheet_container').classList.remove('placeholder')
|
|
getRef('sheet_heading').classList.remove('placeholder')
|
|
getRef('sheet_type').classList.remove('placeholder')
|
|
getRef('sheet_description').classList.remove('placeholder')
|
|
getRef('sheet_container__header').classList.remove('hidden')
|
|
const { id, title, description, editors, attributes, sheet } = logSheet.viewLogs(sheetId)
|
|
renderSheetView({ sheetId: id, title, description, editors, attributes, sheet: sheet.reverse() })
|
|
}).catch(error => notify(error, "error"))
|
|
}
|
|
|
|
function renderPersonList(personList = logSheet.listPersons()) {
|
|
let list = []
|
|
for (person in personList)
|
|
list.push(render.personCard(person, personList[person], getRef("people_container")))
|
|
renderElem(getRef("people_container"), html`${list}`)
|
|
}
|
|
|
|
function renderSheetList() {
|
|
const sheetList = logSheet.listSheets()
|
|
const list = []
|
|
if (floGlobals.isSubAdmin) {
|
|
const firstCard = html`
|
|
<div id="add_new_sheet" class="sheet-card interactive" onclick="openPopup('new_sheet_popup')">
|
|
<div class="sheet-card__icon">
|
|
<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 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/></svg>
|
|
</div>
|
|
<h4>Add new sheet</h4>
|
|
</div>
|
|
`;
|
|
list.push(firstCard)
|
|
}
|
|
for (const sheet in sheetList) {
|
|
list.push(render.sheetCard(sheet, sheetList[sheet]))
|
|
}
|
|
renderElem(getRef('sheets_container'), html`${list}`)
|
|
}
|
|
let startingIndex = 0,
|
|
endingIndex = 0
|
|
|
|
function renderSheetView(details = {}) {
|
|
const { sheetId, title, description, editors, attributes, sheet, onlyRenderTable = false, lazyLoad = false } = details
|
|
console.log(sheet)
|
|
|
|
const isWriteable = !editors || editors.includes(myFloID) || floGlobals.isSubAdmin
|
|
const lazyLoadRows = 4
|
|
if (!lazyLoad)
|
|
floGlobals.currentSheet = { sheetId, title, description, editors, attributes, sheet, }
|
|
if (lazyLoad) {
|
|
startingIndex = sheet.length > endingIndex ? endingIndex : sheet.length
|
|
endingIndex = sheet.length - endingIndex > lazyLoadRows ? endingIndex + lazyLoadRows : sheet.length
|
|
} else {
|
|
startingIndex = 0
|
|
endingIndex = sheet.length > lazyLoadRows ? lazyLoadRows : sheet.length
|
|
}
|
|
if (!onlyRenderTable) {
|
|
//Add Sheet Details
|
|
getRef('sheet_heading').textContent = title;
|
|
getRef('sheet_type').textContent = editors ? 'Private' : 'Public';
|
|
renderElem(getRef('sheet_editors'), html`${editors ? html`Maintained by: ${editors.map(editor => html`<div class="editor">${editor}</div>`)}` : ''}`)
|
|
getRef('sheet_description').textContent = description;
|
|
let addLogRow = null
|
|
//Add input fields if writable
|
|
if (isWriteable && !lazyLoad) {
|
|
const blockEditingID = editors ? false : !floGlobals.isSubAdmin;
|
|
addLogRow = html`
|
|
<tr>
|
|
<td>${html`<input form="new-log" class="log-input" style="width: 22rem" value=${blockEditingID ? myFloID : ''} ?readonly=${blockEditingID}>`}</td>
|
|
<td></td>
|
|
${attributes.map(attr => html`<td><input form="new-log" class="log-input"></td>`)}
|
|
</tr>
|
|
`;
|
|
}
|
|
renderElem(getRef('sheet_container__header'), html`
|
|
<tr>
|
|
<th>FLO ID</th>
|
|
<th>Grade</th>
|
|
${attributes.map(attr => html`<th>${attr}</th>`)}
|
|
</tr>
|
|
${addLogRow}
|
|
`)
|
|
|
|
}
|
|
if (onlyRenderTable) {
|
|
if (!lazyLoad)
|
|
getRef('sheet_container__body').innerHTML = ''
|
|
}
|
|
const parseVectorClock = (vc) => {
|
|
const [vectorClock, floID] = vc.split('_')
|
|
let time = getFormattedTime(parseInt(vectorClock));
|
|
return `by ${floID} (${time})`
|
|
}
|
|
const createGradeField = (grade) => {
|
|
const isDisabled = (!isWriteable || (!floGlobals.isSubAdmin && !floGlobals.isTrustedID))
|
|
return html`<input type="number" class="grade-input" min="0" max="100" value=${grade} ?disabled=${isDisabled}>`
|
|
}
|
|
for (let i = startingIndex; i < endingIndex; i++) {
|
|
const { log, floID, vc, grade } = sheet[i]
|
|
if (!log || !floID || !vc)
|
|
continue;
|
|
frag.append(html.node`
|
|
<tr title="${parseVectorClock(vc)}" .dataset=${{ vc }}>
|
|
<td>${floID}</td>
|
|
<td>${createGradeField(grade)}</td>
|
|
${log.map(l => html`<td>${l}</td>`)}
|
|
</tr>
|
|
`)
|
|
}
|
|
getRef('sheet_container__body').append(frag)
|
|
if (!onlyRenderTable) {
|
|
//Add options for groupBy
|
|
renderElem(getRef('process_by_attribute_selector'), html`${attributes.map(attr => html`<sm-option value="${attr}" class="wrap-around">${attr}</sm-option>`)}`)
|
|
getRef('sheet_processing_options').classList.remove('hidden')
|
|
}
|
|
}
|
|
|
|
delegate(getRef('sheet_container'), 'keydown', 'input[type="number"]', e => {
|
|
if (e.key.length === 1) {
|
|
if (e.key === '.' && (e.target.value.includes('.') || e.target.value.length === 0)) {
|
|
e.preventDefault();
|
|
} else if (!['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.'].includes(e.key)) {
|
|
e.preventDefault();
|
|
}
|
|
} else if (e.key === 'Enter' && floGlobals.isSubAdmin) {
|
|
// Grade an entry
|
|
e.preventDefault();
|
|
e.delegateTarget.disabled = true;
|
|
const vc = e.delegateTarget.closest('tr').dataset.vc;
|
|
let grade = e.delegateTarget.value.trim();
|
|
if (grade === '' || grade == 0)
|
|
grade = null
|
|
logSheet.gradeLog(floGlobals.currentSheet.sheetId, vc, grade)
|
|
.then(result => notify("Graded Log", 'success'))
|
|
.catch(error => notify("Grading failed: " + error, "error"))
|
|
.finally(_ => e.delegateTarget.disabled = false)
|
|
}
|
|
})
|
|
|
|
function renderGroupByView(groupName, groupData) {
|
|
renderElem(getRef("processed_sheet_view"), html`
|
|
<table class="margin-top-1">
|
|
<thead>
|
|
<tr>
|
|
<th>FLO ID</th>
|
|
<th>${groupName}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${Object.keys(groupData).map(floID => html`
|
|
<tr>
|
|
<td>${floID}</td>
|
|
<td>${groupData[floID]}</td>
|
|
</tr>
|
|
`)}
|
|
</tbody>
|
|
</table>
|
|
`)
|
|
}
|
|
|
|
function sortTable(n, ascending) {
|
|
if (ascending) {
|
|
if (n === 0)
|
|
floGlobals.currentSheet.sheet.sort((a, b) => a.floID.toLowerCase().localeCompare(b.floID.toLowerCase()))
|
|
else if (n === 1)
|
|
floGlobals.currentSheet.sheet.sort((a, b) => {
|
|
if (a.grade === b.grade) {
|
|
return 0;
|
|
} else {
|
|
return (a.grade < b.grade) ? -1 : 1;
|
|
}
|
|
})
|
|
else
|
|
floGlobals.currentSheet.sheet.sort((a, b) => {
|
|
if (isNaN(a.log[n - 2])) {
|
|
let x = (a.log[n - 2]) ? a.log[n - 2].toLowerCase() : a.log[n - 2]
|
|
let y = (b.log[n - 2]) ? b.log[n - 2].toLowerCase() : b.log[n - 2]
|
|
if (x === y) {
|
|
return 0;
|
|
} else {
|
|
return (x < y) ? -1 : 1;
|
|
}
|
|
} else
|
|
return a.log[n - 2] - b.log[n - 2]
|
|
})
|
|
} else {
|
|
if (n === 0)
|
|
floGlobals.currentSheet.sheet.sort((a, b) => b.floID.toLowerCase().localeCompare(a.floID.toLowerCase()))
|
|
else if (n === 1)
|
|
floGlobals.currentSheet.sheet.sort((a, b) => {
|
|
if (a.grade === b.grade) {
|
|
return 0;
|
|
} else {
|
|
return (a.grade > b.grade) ? -1 : 1;
|
|
}
|
|
})
|
|
else
|
|
floGlobals.currentSheet.sheet.sort((a, b) => {
|
|
if (isNaN(a.log[n - 2])) {
|
|
let x = (a.log[n - 2]) ? a.log[n - 2].toLowerCase() : a.log[n - 2]
|
|
let y = (b.log[n - 2]) ? b.log[n - 2].toLowerCase() : b.log[n - 2]
|
|
if (x === y) {
|
|
return 0;
|
|
} else {
|
|
return (x > y) ? -1 : 1;
|
|
}
|
|
} else
|
|
return b.log[n - 2] - a.log[n - 2]
|
|
})
|
|
}
|
|
renderSheetView({ ...floGlobals.currentSheet, onlyRenderTable: true })
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |