flologsheet/index.html
2022-09-17 19:36:03 +05:30

1451 lines
67 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/css?family=Roboto:300,400,500,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: "FKAEdnPfjXLHSYwrXQu377ugN4tXU7VGdf", //"FRaBr5F665RVkQ1A1EYrMfbX2UF52vWjKr",
application: "TEST_MODE", // "LogSheet",
// for storing signle log sheet data
currentSheet: {}
}
</script>
<script src="scripts/lib.js"></script>
<script src="scripts/floCrypto.js"></script>
<script src="scripts/floBlockchainAPI.js"></script>
<script src="scripts/compactIDB.js"></script>
<script src="scripts/floCloudAPI.js"></script>
<script src="scripts/floDapps.js"></script>
<script src="scripts/logsheet.js"></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)
getRef("user_flo_id").value = myFloID
console.log(`Downloading objectData! Please Wait...`);
//request object data from Supernode
logSheet.init().then(result => {
console.log(result)
//Render the personList and sheetList
renderPersonList()
renderSheetList()
//hide loading screen
hideLoader()
showPage(window.location.hash, { firstLoad: true })
//display add buttons if subAdmin, else hide
if (floGlobals.subAdmins.includes(myFloID)) {
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"></p>
<div class="flex align-center">
<sm-button variant="no-outline" class="cancel-button">Cancel</sm-button>
<sm-button variant="no-outline" class="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 &bull; Distributed &bull; Reliable</p>
</div>
<div class="sign-in-box flex flex-direction-column gap-2">
<strip-select id="entry_type_selector" class="align-self-start">
<strip-option value="sign-in" selected>Sign in</strip-option>
<strip-option value="sign-up">Sign up</strip-option>
</strip-select>
<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 align-center space-between gap-1">
<div class="flex flex-direction-column">
<h5>RanchiMall</h5>
<h4>FLO LogSheet</h4>
</div>
<theme-toggle class="margin-left-auto"></theme-toggle>
<button id="user_icon" onclick="openPopup('user_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="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.09-.16-.26-.25-.44-.25-.06 0-.12.01-.17.03l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.06-.02-.12-.03-.18-.03-.17 0-.34.09-.43.25l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.09.16.26.25.44.25.06 0 .12-.01.17-.03l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.06.02.12.03.18.03.17 0 .34-.09.43-.25l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zm-1.98-1.71c.04.31.05.52.05.73 0 .21-.02.43-.05.73l-.14 1.13.89.7 1.08.84-.7 1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2 1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43c-.43-.18-.83-.41-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21 1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21 1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16 1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14 1.13zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" />
</svg>
</button>
</div>
</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>
<button class="button button--small margin-left-auto" onclick="openPopup('group_by')">Group
by</button>
</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="user_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>Settings</h3>
</header>
<section>
<div class="grid gap-1">
<div class="grid gap-0-3">
<b>My FLO address</b>
<sm-copy id="user_flo_id"></sm-copy>
</div>
<button class="button button--danger margin-right-auto" onclick="signOut()">Sign out</button>
</div>
</section>
</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="group_by">
<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="group_by_title"> </h3>
</header>
<div class="flex align-center" id="group_by_menu">
<sm-select id="first_select">
<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="second_select"></sm-select>
</div>
<div class="table-container" id="group_by_view"></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}`)
getRef(popupId).show({ pinned })
return getRef(popupId);
}
// hides the popup or modal
function closePopup() {
if (popupStack.peek() === undefined)
return;
popupStack.peek().popup.hide()
}
// 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;
const cancelButton = getRef('confirmation_popup').querySelector('.cancel-button');
const confirmButton = getRef('confirmation_popup').querySelector('.confirm-button')
confirmButton.textContent = confirmText
cancelButton.textContent = cancelText
confirmButton.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>`
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 => {
let popup = e.detail.popup
switch (popup.id) {
case 'group_by':
renderGroup()
break;
}
})
document.addEventListener('popupclosed', e => {
let popup = e.detail.popup
switch (popup.id) {
case 'new_sheet_popup':
getRef('columns_container').innerHTML = ''
getRef('editors_container').innerHTML = ''
break;
case 'group_by':
renderElem(getRef("group_by_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 observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
renderSheetView({ ...floGlobals.currentSheet, onlyRenderTable: true, lazyLoad: true })
observer.disconnect()
}
})
})
const watchLastElement = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
observer.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
}
}
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('/')) {
if (targetPage.includes('?')) {
const splitAddress = targetPage.split('?')
searchParams = splitAddress.pop()
const pages = splitAddress.pop().split('/')
pageId = pages[1]
subPageId1 = pages[2]
subPageId2 = pages[3]
} else {
const pages = targetPage.split('/')
pageId = pages[1]
subPageId1 = pages[2]
subPageId2 = pages[3]
}
} 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')
break;
case 'sheet_view':
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))
})
getRef('first_select').addEventListener("change", renderGroup)
getRef('second_select').addEventListener("change", renderGroup)
function renderGroup() {
try {
const type = getRef('first_select').value
const column = getRef('second_select').value
const { sheetId, sheet } = floGlobals.currentSheet
if (type === 'count') {
getRef('second_select').classList.add("hidden")
let data = logSheet.groupBy.count(sheetId, sheet)
renderGroupByView("count", data)
} else {
getRef('second_select').classList.remove("hidden")
let data = logSheet.groupBy[type](sheetId, sheet, column);
renderGroupByView(`${type}(${column})`, data)
}
} catch (error) {
notify(error, "error")
}
}
function clearElement(element) {
element.innerHTML = '';
return element;
}
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 => {
allFormElements.forEach(element => element.disabled = false)
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")
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')
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.subAdmins.includes(myFloID)) {
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}`)
}
function createTh(text) {
let cell = document.createElement('th')
cell.textContent = text
return cell
}
function createCell(text) {
let cell = document.createElement('td')
cell.textContent = text
return cell
}
let startingIndex = 0,
endingIndex = 0
function renderSheetView(details = {}) {
const { sheetId, title, description, editors, attributes, sheet, onlyRenderTable = false, lazyLoad = false } = details
const isWriteable = !editors || editors.includes(myFloID)
const isSubAdmin = floGlobals.subAdmins.includes(myFloID)
if (!lazyLoad)
floGlobals.currentSheet = { sheetId, title, description, editors, attributes, sheet, }
const parseVectorClock = (vc) => {
const [vectorClock, floID] = vc.split('_')
let time = new Date(parseInt(vectorClock)).toString().slice(4, 24);
return `by ${floID} (${time})`
}
const createGradeField = (vc, grade) => {
let gradeField = document.createElement("input")
gradeField.setAttribute("type", "text")
gradeField.className = "grade-input"
gradeField.value = grade
if (!isWriteable || !isSubAdmin || vc.split('_')[1] == myFloID)
gradeField.disabled = true;
else {
gradeField.addEventListener("keydown", (event) => {
if (event.key === 'Enter') {
event.preventDefault();
gradeField.disabled = true;
logSheet.gradeLog(sheetId, vc, gradeField.value)
.then(result => notify("Graded Log"))
.catch(error => notify("Grading failed: " + error, "error"))
.finally(_ => gradeField.disabled = false)
}
})
}
return gradeField
}
if (lazyLoad) {
startingIndex = sheet.length > endingIndex ? endingIndex : sheet.length
endingIndex = sheet.length - endingIndex > 20 ? endingIndex + 20 : sheet.length
} else {
startingIndex = 0
endingIndex = sheet.length > 20 ? 20 : 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 blockEditingID = (!isSubAdmin && (editors || true));
let addLogRow = null
//Add input fields if writable
if (isWriteable && !lazyLoad) {
addLogRow = html`
<tr>
<td>${blockEditingID ? myFloID : html`<input form="new-log" class="log-input">`}</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 = ''
}
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)}">
<td>${floID}</td>
<td>${createGradeField(vc, grade)}</td>
${log.map(l => html`<td>${l}</td>`)}
</tr>
`)
}
getRef('sheet_container__body').append(frag)
if (!onlyRenderTable) {
//Add options for groupBy
getRef('group_by_title').textContent = `Group ${title} by`;
let colSelect = clearElement(getRef('group_by_menu').children[1]);
attributes.forEach(a => {
let opt = document.createElement('sm-option');
opt.setAttribute('value', a);
opt.textContent = a;
colSelect.appendChild(opt)
})
}
}
function renderGroupByView(groupName, groupData) {
renderElem(getRef("group_by_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>