Feature update and UX improvements

-- added sorting options for article explorer popup (sort alphabetically or chronologically )
- added page routing with article ID to enable history navigation

components
- added focus trapping for sm-popup (UX)
This commit is contained in:
sairaj mote 2021-11-21 19:51:25 +05:30
parent e155bfc341
commit 70b13b7f5a
5 changed files with 723 additions and 96 deletions

View File

@ -1007,7 +1007,6 @@ smPopup.innerHTML = `
--height: auto; --height: auto;
--min-width: auto; --min-width: auto;
--min-height: auto; --min-height: auto;
--body-padding: 1.5rem;
--backdrop-background: rgba(0, 0, 0, 0.6); --backdrop-background: rgba(0, 0, 0, 0.6);
--border-radius: 0.8rem 0.8rem 0 0; --border-radius: 0.8rem 0.8rem 0 0;
} }
@ -1087,13 +1086,13 @@ smPopup.innerHTML = `
-ms-flex: 1; -ms-flex: 1;
flex: 1; flex: 1;
width: 100%; width: 100%;
padding: var(--body-padding); padding: var(--body-padding, 1.5rem);
overflow-y: auto; overflow-y: auto;
} }
.hide{ .hide{
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
visiblity: none; visibility: none;
} }
@media screen and (min-width: 640px){ @media screen and (min-width: 640px){
:host{ :host{
@ -1169,7 +1168,10 @@ customElements.define('sm-popup', class extends HTMLElement {
this.touchEndY = 0; this.touchEndY = 0;
this.touchStartTime = 0; this.touchStartTime = 0;
this.touchEndTime = 0; this.touchEndTime = 0;
this.touchEndAnimataion = undefined; this.touchEndAnimation = undefined;
this.focusable
this.autoFocus
this.mutationObserver
this.popupContainer = this.shadowRoot.querySelector('.popup-container'); this.popupContainer = this.shadowRoot.querySelector('.popup-container');
this.popup = this.shadowRoot.querySelector('.popup'); this.popup = this.shadowRoot.querySelector('.popup');
@ -1183,6 +1185,7 @@ customElements.define('sm-popup', class extends HTMLElement {
this.handleTouchMove = this.handleTouchMove.bind(this); this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this); this.handleTouchEnd = this.handleTouchEnd.bind(this);
this.movePopup = this.movePopup.bind(this); this.movePopup = this.movePopup.bind(this);
this.detectFocus = this.detectFocus.bind(this);
} }
static get observedAttributes() { static get observedAttributes() {
@ -1231,6 +1234,8 @@ customElements.define('sm-popup', class extends HTMLElement {
this.popup.style.transform = 'none'; this.popup.style.transform = 'none';
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
document.body.style.top = `-${window.scrollY}px`; document.body.style.top = `-${window.scrollY}px`;
const elementToFocus = this.autoFocus || this.focusable[0];
elementToFocus.tagName.includes('SM-') ? elementToFocus.focusIn() : elementToFocus.focus();
return this.popupStack; return this.popupStack;
} }
hide() { hide() {
@ -1279,13 +1284,13 @@ customElements.define('sm-popup', class extends HTMLElement {
handleTouchMove(e) { handleTouchMove(e) {
if (this.touchStartY < e.changedTouches[0].clientY) { if (this.touchStartY < e.changedTouches[0].clientY) {
this.offset = e.changedTouches[0].clientY - this.touchStartY; this.offset = e.changedTouches[0].clientY - this.touchStartY;
this.touchEndAnimataion = window.requestAnimationFrame(() => this.movePopup()); this.touchEndAnimation = window.requestAnimationFrame(() => this.movePopup());
} }
} }
handleTouchEnd(e) { handleTouchEnd(e) {
this.touchEndTime = e.timeStamp; this.touchEndTime = e.timeStamp;
cancelAnimationFrame(this.touchEndAnimataion); cancelAnimationFrame(this.touchEndAnimation);
this.touchEndY = e.changedTouches[0].clientY; this.touchEndY = e.changedTouches[0].clientY;
this.popup.style.transition = 'transform 0.3s'; this.popup.style.transition = 'transform 0.3s';
this.threshold = this.popup.getBoundingClientRect().height * 0.3; this.threshold = this.popup.getBoundingClientRect().height * 0.3;
@ -1314,6 +1319,20 @@ customElements.define('sm-popup', class extends HTMLElement {
this.popup.style.transform = `translateY(${this.offset}px)`; this.popup.style.transform = `translateY(${this.offset}px)`;
} }
detectFocus(e) {
if (e.code === 'Tab') {
const lastElement = this.focusable[this.focusable.length - 1];
const firstElement = this.focusable[0];
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.tagName.includes('SM-') ? lastElement.focusIn() : lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.tagName.includes('SM-') ? firstElement.focusIn() : firstElement.focus();
}
}
}
connectedCallback() { connectedCallback() {
this.popupBodySlot.addEventListener('slotchange', () => { this.popupBodySlot.addEventListener('slotchange', () => {
this.forms = this.querySelectorAll('sm-form'); this.forms = this.querySelectorAll('sm-form');
@ -1340,16 +1359,26 @@ customElements.define('sm-popup', class extends HTMLElement {
}); });
resizeObserver.observe(this); resizeObserver.observe(this);
this.mutationObserver = new MutationObserver(entries => {
entries.forEach(mutation => {
this.focusable = this.querySelectorAll('sm-button:not([disabled]), button:not([disabled]), [href], sm-input, input, sm-select, select, sm-checkbox, sm-textarea, textarea, [tabindex]:not([tabindex="-1"])')
this.autoFocus = this.querySelector('[autofocus]')
})
})
this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true })
this.addEventListener('keydown', this.detectFocus);
this.popupHeader.addEventListener('touchstart', this.handleTouchStart, { passive: true }); this.popupHeader.addEventListener('touchstart', this.handleTouchStart, { passive: true });
this.popupHeader.addEventListener('touchmove', this.handleTouchMove, { passive: true }); this.popupHeader.addEventListener('touchmove', this.handleTouchMove, { passive: true });
this.popupHeader.addEventListener('touchend', this.handleTouchEnd, { passive: true }); this.popupHeader.addEventListener('touchend', this.handleTouchEnd, { passive: true });
} }
disconnectedCallback() { disconnectedCallback() {
this.removeEventListener('keydown', this.detectFocus);
this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, { passive: true }); this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, { passive: true }); this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, { passive: true });
this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, { passive: true }); this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, { passive: true });
resizeObserver.unobserve(); resizeObserver.unobserve();
this.mutationObserver.disconnect()
} }
attributeChangedCallback(name) { attributeChangedCallback(name) {
if (name === 'open') { if (name === 'open') {
@ -2329,7 +2358,7 @@ smSelect.innerHTML = `
} }
} }
</style> </style>
<div class="select" > <div class="select">
<div class="selection"> <div class="selection">
<div class="selected-option-text"></div> <div class="selected-option-text"></div>
<svg class="icon toggle" 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 13.172l4.95-4.95 1.414 1.414L12 16 5.636 9.636 7.05 8.222z"/></svg> <svg class="icon toggle" 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 13.172l4.95-4.95 1.414 1.414L12 16 5.636 9.636 7.05 8.222z"/></svg>
@ -2345,6 +2374,7 @@ customElements.define('sm-select', class extends HTMLElement {
mode: 'open' mode: 'open'
}).append(smSelect.content.cloneNode(true)) }).append(smSelect.content.cloneNode(true))
this.focusIn = this.focusIn.bind(this)
this.reset = this.reset.bind(this) this.reset = this.reset.bind(this)
this.open = this.open.bind(this) this.open = this.open.bind(this)
this.collapse = this.collapse.bind(this) this.collapse = this.collapse.bind(this)
@ -2413,6 +2443,10 @@ customElements.define('sm-select', class extends HTMLElement {
} }
} }
focusIn() {
this.selection.focus()
}
open() { open() {
this.optionList.classList.remove('hide') this.optionList.classList.remove('hide')
this.optionList.animate(this.slideDown, this.animationOptions) this.optionList.animate(this.slideDown, this.animationOptions)
@ -2757,6 +2791,10 @@ customElements.define('sm-checkbox', class extends HTMLElement {
return this.getAttribute('value') return this.getAttribute('value')
} }
focusIn() {
this.focus()
}
reset() { reset() {
this.removeAttribute('checked') this.removeAttribute('checked')
} }
@ -3447,7 +3485,6 @@ textField.innerHTML = `
align-items: center; align-items: center;
} }
.text{ .text{
padding: 0.6rem 0;
transition: background-color 0.3s; transition: background-color 0.3s;
border-bottom: 0.15rem solid transparent; border-bottom: 0.15rem solid transparent;
overflow-wrap: break-word; overflow-wrap: break-word;
@ -3658,3 +3695,313 @@ customElements.define('text-field', class extends HTMLElement {
this.saveButton.removeEventListener('click', this.setNonEditable) this.saveButton.removeEventListener('click', this.setNonEditable)
} }
}) })
const smMenu = document.createElement('template')
smMenu.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
:host{
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
}
.menu{
display: -ms-grid;
display: grid;
place-items: center;
height: 2rem;
width: 2rem;
outline: none;
}
.icon {
position: absolute;
fill: rgba(var(--text-color), 0.7);
height: 2.4rem;
width: 2.4rem;
padding: 0.5rem;
border-radius: 2rem;
-webkit-transition: background 0.3s;
-o-transition: background 0.3s;
transition: background 0.3s;
}
.select{
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
cursor: pointer;
width: 100%;
-webkit-tap-highlight-color: transparent;
}
.menu:focus .icon,
.focused{
background: rgba(var(--text-color), 0.1);
}
:host([align-options="left"]) .options{
left: 0;
}
:host([align-options="right"]) .options{
right: 0;
}
.options{
top: 100%;
padding: 0.3rem;
overflow: hidden auto;
position: absolute;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
min-width: -webkit-max-content;
min-width: -moz-max-content;
min-width: max-content;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
background: var(--background, rgba(var(--background-color), 1));
border-radius: var(--border-radius, 0.5rem);
z-index: 1;
-webkit-box-shadow: 0 0.5rem 1.5rem -0.5rem rgba(0,0,0,0.3);
box-shadow: 0 0.5rem 1.5rem -0.5rem rgba(0,0,0,0.3);
bottom: auto;
}
.hide{
display: none;
}
@media screen and (max-width: 640px){
.options{
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: auto;
border-radius: 0.5rem 0.5rem 0 0;
}
}
@media (hover: hover){
.menu:hover .icon{
background: rgba(var(--text-color), 0.1);
}
}
</style>
<div class="select">
<div class="menu" tabindex="0">
<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 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
</div>
<div class="options hide">
<slot></slot>
</div>
</div>`;
customElements.define('sm-menu', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smMenu.content.cloneNode(true))
this.isOpen = false;
this.availableOptions
this.containerDimensions
this.animOptions = {
duration: 200,
easing: 'ease'
}
this.optionList = this.shadowRoot.querySelector('.options')
this.menu = this.shadowRoot.querySelector('.menu')
this.icon = this.shadowRoot.querySelector('.icon')
this.expand = this.expand.bind(this)
this.collapse = this.collapse.bind(this)
this.toggle = this.toggle.bind(this)
this.handleKeyDown = this.handleKeyDown.bind(this)
this.handleClickOutside = this.handleClickOutside.bind(this)
}
static get observedAttributes() {
return ['value']
}
get value() {
return this.getAttribute('value')
}
set value(val) {
this.setAttribute('value', val)
}
expand() {
if (!this.isOpen) {
this.optionList.classList.remove('hide')
this.optionList.animate([
{
transform: window.innerWidth < 640 ? 'translateY(1.5rem)' : 'translateY(-1rem)',
opacity: '0'
},
{
transform: 'none',
opacity: '1'
},
], this.animOptions)
.onfinish = () => {
this.isOpen = true
this.icon.classList.add('focused')
}
}
}
collapse() {
if (this.isOpen) {
this.optionList.animate([
{
transform: 'none',
opacity: '1'
},
{
transform: window.innerWidth < 640 ? 'translateY(1.5rem)' : 'translateY(-1rem)',
opacity: '0'
},
], this.animOptions)
.onfinish = () => {
this.isOpen = false
this.icon.classList.remove('focused')
this.optionList.classList.add('hide')
}
}
}
toggle() {
if (!this.isOpen) {
this.expand()
} else {
this.collapse()
}
}
handleKeyDown(e) {
// If key is pressed on menu button
if (e.target === this) {
if (e.code === 'ArrowDown') {
e.preventDefault()
this.availableOptions[0].focus()
}
else if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault()
this.toggle()
}
} else { // If key is pressed over menu options
if (e.code === 'ArrowUp') {
e.preventDefault()
if (document.activeElement.previousElementSibling) {
document.activeElement.previousElementSibling.focus()
} else {
this.availableOptions[this.availableOptions.length - 1].focus()
}
}
else if (e.code === 'ArrowDown') {
e.preventDefault()
if (document.activeElement.nextElementSibling) {
document.activeElement.nextElementSibling.focus()
} else {
this.availableOptions[0].focus()
}
}
else if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault()
e.target.click()
}
}
}
handleClickOutside(e) {
if (!this.contains(e.target) && e.button !== 2) {
this.collapse()
}
}
connectedCallback() {
this.setAttribute('role', 'listbox')
this.setAttribute('aria-label', 'dropdown menu')
const slot = this.shadowRoot.querySelector('.options slot')
slot.addEventListener('slotchange', e => {
this.availableOptions = e.target.assignedElements()
this.containerDimensions = this.optionList.getBoundingClientRect()
});
this.addEventListener('click', this.toggle)
this.addEventListener('keydown', this.handleKeyDown)
document.addEventListener('mousedown', this.handleClickOutside)
}
disconnectedCallback() {
this.removeEventListener('click', this.toggle)
this.removeEventListener('keydown', this.handleKeyDown)
document.removeEventListener('mousedown', this.handleClickOutside)
}
})
// option
const menuOption = document.createElement('template')
menuOption.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
:host{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.option{
display: -webkit-box;
display: -ms-flexbox;
display: flex;
min-width: 100%;
padding: var(--padding, 0.6rem 1rem);
cursor: pointer;
overflow-wrap: break-word;
white-space: nowrap;
outline: none;
user-select: none;
border-radius: 0.3rem;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
:host(:focus){
outline: none;
background: rgba(var(--text-color), 0.1);
}
@media (any-hover: hover){
.option{
transition: background-color 0.2s;
}
.option:hover{
background: rgba(var(--text-color), 0.1);
}
}
</style>
<div class="option">
<slot></slot>
</div>`;
customElements.define('menu-option', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(menuOption.content.cloneNode(true))
}
connectedCallback() {
this.setAttribute('role', 'option')
this.setAttribute('tabindex', '0')
this.addEventListener('keyup', e => {
if (e.code === 'Enter' || e.code === 'Space') {
e.preventDefault()
this.click()
}
})
}
})

View File

@ -22,8 +22,7 @@ body {
} }
body, body,
body * { body * {
--accent-color: #504dff; --accent-color: rgb(0, 156, 78);
--accent-color--light: #eeeeff;
--text-color: 36, 36, 36; --text-color: 36, 36, 36;
--background-color: 248, 248, 248; --background-color: 248, 248, 248;
--foreground-color: rgb(255, 255, 255); --foreground-color: rgb(255, 255, 255);
@ -36,8 +35,7 @@ body * {
body[data-theme=dark], body[data-theme=dark],
body[data-theme=dark] * { body[data-theme=dark] * {
--accent-color: #a3a1ff; --accent-color: rgb(14, 230, 122);
--accent-color--light: rgba(142, 140, 255, 0.06);
--text-color: 230, 230, 230; --text-color: 230, 230, 230;
--text-color-light: 170, 170, 170; --text-color-light: 170, 170, 170;
--background-color: 10, 10, 10; --background-color: 10, 10, 10;
@ -501,6 +499,14 @@ sm-checkbox {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
sm-menu {
--background: var(--foreground-color);
}
menu-option {
font-size: 0.9rem;
}
.warning { .warning {
background-color: khaki; background-color: khaki;
color: rgba(0, 0, 0, 0.7); color: rgba(0, 0, 0, 0.7);
@ -524,11 +530,11 @@ sm-checkbox {
#main_header { #main_header {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
padding: 1rem 1.5rem; padding: 1rem;
-webkit-box-align: center; -webkit-box-align: center;
-ms-flex-align: center; -ms-flex-align: center;
align-items: center; align-items: center;
grid-template-columns: auto 1fr auto auto; grid-template-columns: 1fr auto auto;
grid-column: 1/-1; grid-column: 1/-1;
background-color: var(--foreground-color); background-color: var(--foreground-color);
} }
@ -556,6 +562,24 @@ sm-checkbox {
font-weight: 700; font-weight: 700;
} }
#article_list_popup {
--width: min(64rem, 100%);
--min-height: 70vh;
}
#article_list_popup::part(popup-header), #article_list_popup::part(popup-body) {
padding: 0.5rem;
}
#article_list_popup .popup__header {
padding: 1rem;
gap: 1rem;
padding-bottom: 0;
grid-template-columns: minmax(0, 1fr);
}
#article_list {
grid-template-columns: repeat(auto-fill, minmax(30ch, 1fr));
}
.article-link { .article-link {
padding: 1rem; padding: 1rem;
color: rgba(var(--text-color), 0.8); color: rgba(var(--text-color), 0.8);
@ -577,6 +601,40 @@ sm-checkbox {
color: rgba(var(--text-color), 0.8); color: rgba(var(--text-color), 0.8);
} }
#edit_sections_popup {
--body-padding: 1.2rem;
}
#section_list_container {
background-color: rgba(var(--text-color), 0.06);
margin: 1rem 0;
padding: 0 0.8rem;
border-radius: 0.5rem;
}
.section-card {
font-weight: 500;
}
.section-card:not(.section-card--new) {
padding: 0.8rem 0;
}
.section-card--new input {
border: none;
font-size: inherit;
background: inherit;
color: inherit;
width: 100%;
padding: 0.8rem 0;
}
.section-card--new input:focus {
outline: var(--accent-color) solid;
}
#insert_section_button {
-ms-flex-item-align: start;
align-self: flex-start;
}
#article_wrapper { #article_wrapper {
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
@ -608,7 +666,7 @@ sm-checkbox {
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
gap: 0.5rem; gap: 1rem;
overflow-x: auto; overflow-x: auto;
-ms-flex-negative: 0; -ms-flex-negative: 0;
flex-shrink: 0; flex-shrink: 0;
@ -623,7 +681,7 @@ sm-checkbox {
} }
.content-card { .content-card {
scroll-snap-align: center; scroll-snap-align: start;
width: min(46ch, 100%); width: min(46ch, 100%);
-ms-flex-negative: 0; -ms-flex-negative: 0;
flex-shrink: 0; flex-shrink: 0;
@ -806,7 +864,8 @@ sm-checkbox {
background-color: #00e67650; background-color: #00e67650;
} }
.entry__changes .removed { .entry__changes .removed {
background-color: #ff3a4a50; -webkit-text-decoration-color: red;
text-decoration-color: red;
} }
@media screen and (max-width: 40rem) and (any-hover: none) { @media screen and (max-width: 40rem) and (any-hover: none) {
@ -819,6 +878,17 @@ sm-checkbox {
--padding: 0.9rem 1.6rem; --padding: 0.9rem 1.6rem;
} }
#article_name_wrapper {
grid-row: 2/3;
grid-column: 1/-1;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
}
#article_name_wrapper sm-menu {
margin-left: auto;
}
.hide-on-mobile { .hide-on-mobile {
display: none; display: none;
} }
@ -861,6 +931,11 @@ sm-checkbox {
display: none; display: none;
} }
#main_header {
padding: 1rem 1.5rem;
grid-template-columns: auto 1fr auto auto;
}
#main_page.active-sidebar { #main_page.active-sidebar {
height: 100%; height: 100%;
overflow-y: hidden; overflow-y: hidden;
@ -871,6 +946,15 @@ sm-checkbox {
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
} }
#article_list_popup .popup__header {
grid-template-columns: auto 1fr auto;
padding-bottom: 1rem;
}
#article_list_search {
width: 16rem;
}
} }
@media (any-hover: hover) { @media (any-hover: hover) {
::-webkit-scrollbar { ::-webkit-scrollbar {

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -18,8 +18,7 @@ body {
body { body {
&, &,
* { * {
--accent-color: #504dff; --accent-color: rgb(0, 156, 78);
--accent-color--light: #eeeeff;
--text-color: 36, 36, 36; --text-color: 36, 36, 36;
--background-color: 248, 248, 248; --background-color: 248, 248, 248;
--foreground-color: rgb(255, 255, 255); --foreground-color: rgb(255, 255, 255);
@ -37,8 +36,7 @@ body {
body[data-theme="dark"] { body[data-theme="dark"] {
&, &,
* { * {
--accent-color: #a3a1ff; --accent-color: rgb(14, 230, 122);
--accent-color--light: rgba(142, 140, 255, 0.06);
--text-color: 230, 230, 230; --text-color: 230, 230, 230;
--text-color-light: 170, 170, 170; --text-color-light: 170, 170, 170;
--background-color: 10, 10, 10; --background-color: 10, 10, 10;
@ -440,6 +438,12 @@ sm-checkbox {
--width: 1rem; --width: 1rem;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
sm-menu {
--background: var(--foreground-color);
}
menu-option {
font-size: 0.9rem;
}
.warning { .warning {
background-color: khaki; background-color: khaki;
color: rgba(0, 0, 0, 0.7); color: rgba(0, 0, 0, 0.7);
@ -461,9 +465,9 @@ sm-checkbox {
#main_header { #main_header {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
padding: 1rem 1.5rem; padding: 1rem;
align-items: center; align-items: center;
grid-template-columns: auto 1fr auto auto; grid-template-columns: 1fr auto auto;
grid-column: 1/-1; grid-column: 1/-1;
background-color: var(--foreground-color); background-color: var(--foreground-color);
} }
@ -489,6 +493,24 @@ sm-checkbox {
font-weight: 700; font-weight: 700;
} }
#article_list_popup {
--width: min(64rem, 100%);
--min-height: 70vh;
&::part(popup-header),
&::part(popup-body) {
padding: 0.5rem;
}
.popup__header {
padding: 1rem;
gap: 1rem;
padding-bottom: 0;
grid-template-columns: minmax(0, 1fr);
}
}
#article_list {
grid-template-columns: repeat(auto-fill, minmax(30ch, 1fr));
}
.article-link { .article-link {
padding: 1rem; padding: 1rem;
color: rgba(var(--text-color), 0.8); color: rgba(var(--text-color), 0.8);
@ -509,6 +531,38 @@ sm-checkbox {
color: rgba(var(--text-color), 0.8); color: rgba(var(--text-color), 0.8);
} }
#edit_sections_popup {
--body-padding: 1.2rem;
}
#section_list_container {
background-color: rgba(var(--text-color), 0.06);
margin: 1rem 0;
padding: 0 0.8rem;
border-radius: 0.5rem;
}
.section-card {
font-weight: 500;
&:not(.section-card--new) {
padding: 0.8rem 0;
}
&--new {
input {
border: none;
font-size: inherit;
background: inherit;
color: inherit;
width: 100%;
padding: 0.8rem 0;
&:focus {
outline: var(--accent-color) solid;
}
}
}
}
#insert_section_button {
align-self: flex-start;
}
#article_wrapper { #article_wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -531,7 +585,7 @@ sm-checkbox {
.article-section { .article-section {
display: flex; display: flex;
gap: 0.5rem; gap: 1rem;
overflow-x: auto; overflow-x: auto;
flex-shrink: 0; flex-shrink: 0;
scroll-snap-type: x mandatory; scroll-snap-type: x mandatory;
@ -543,7 +597,7 @@ sm-checkbox {
} }
} }
.content-card { .content-card {
scroll-snap-align: center; scroll-snap-align: start;
width: min(46ch, 100%); width: min(46ch, 100%);
flex-shrink: 0; flex-shrink: 0;
border-radius: 0.5rem; border-radius: 0.5rem;
@ -704,7 +758,7 @@ sm-checkbox {
background-color: #00e67650; background-color: #00e67650;
} }
.removed { .removed {
background-color: #ff3a4a50; text-decoration-color: red;
} }
} }
@ -719,6 +773,14 @@ sm-checkbox {
sm-button { sm-button {
--padding: 0.9rem 1.6rem; --padding: 0.9rem 1.6rem;
} }
#article_name_wrapper {
grid-row: 2/3;
grid-column: 1/-1;
justify-content: flex-start;
sm-menu {
margin-left: auto;
}
}
.hide-on-mobile { .hide-on-mobile {
display: none; display: none;
} }
@ -755,6 +817,10 @@ sm-checkbox {
.hide-on-desktop { .hide-on-desktop {
display: none; display: none;
} }
#main_header {
padding: 1rem 1.5rem;
grid-template-columns: auto 1fr auto auto;
}
#main_page { #main_page {
&.active-sidebar { &.active-sidebar {
height: 100%; height: 100%;
@ -767,6 +833,15 @@ sm-checkbox {
} }
} }
} }
#article_list_popup {
.popup__header {
grid-template-columns: auto 1fr auto;
padding-bottom: 1rem;
}
}
#article_list_search {
width: 16rem;
}
} }
@media (any-hover: hover) { @media (any-hover: hover) {
::-webkit-scrollbar { ::-webkit-scrollbar {

View File

@ -12,6 +12,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&display=swap" rel="stylesheet">
<script src="purify.min.js" defer></script> <script src="purify.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.4.6" defer></script>
<script id="floGlobals"> <script id="floGlobals">
/* Constants for FLO blockchain operations !!Make sure to add this at begining!! */ /* Constants for FLO blockchain operations !!Make sure to add this at begining!! */
const floGlobals = { const floGlobals = {
@ -71,23 +72,36 @@
<h4>Content Collab</h4> <h4>Content Collab</h4>
</div> </div>
</div> </div>
<div id="article_name_wrapper" class="flex align-center justify-center"> <div id="article_name_wrapper" class="flex gap-0-5 align-center justify-center">
<button title="Create new article" class="button__icon--left" <button title="Show all articles list" onclick="showPopup('article_list_popup')">
onclick="showPopup('create_article_popup')">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" <svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
fill="#000000"> fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" /> <path d="M0 0h24v24H0V0z" fill="none" />
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /> <path
d="M4 8h4V4H4v4zm6 12h4v-4h-4v4zm-6 0h4v-4H4v4zm0-6h4v-4H4v4zm6 0h4v-4h-4v4zm6-10v4h4V4h-4zm-6 4h4V4h-4v4zm6 6h4v-4h-4v4zm0 6h4v-4h-4v4z" />
</svg> </svg>
</button> </button>
<text-field id="current_article_title"></text-field> <text-field id="current_article_title"></text-field>
<button class="button__icon--right" onclick="showPopup('article_list_popup')"> <sm-menu align-options="right">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" <menu-option onclick="showPopup('create_article_popup')">
fill="#000000"> <svg class="icon button__icon--left" xmlns="http://www.w3.org/2000/svg" height="24px"
<path d="M24 24H0V0h24v24z" fill="none" opacity=".87" /> viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z" /> <path d="M0 0h24v24H0V0z" fill="none" />
</svg> <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</button> </svg>
Create new article
</menu-option>
<menu-option onclick="showPopup('edit_sections_popup')">
<svg class="icon button__icon--left" title="edit" xmlns="http://www.w3.org/2000/svg"
height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0z" fill="none"></path>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z">
</path>
</svg>
Edit sections
</menu-option>
</sm-menu>
</div> </div>
<theme-toggle></theme-toggle> <theme-toggle></theme-toggle>
<button onclick="showPopup('user_popup')"> <button onclick="showPopup('user_popup')">
@ -134,25 +148,6 @@
</button> </button>
</div> </div>
<div id="article_wrapper"></div> <div id="article_wrapper"></div>
<!-- <div class="grid page-layout">
<div id="action_button_group" class="flex align-center gap-0-5">
<span>
<b>
Add
</b>
</span>
<button class="actionable-button" title="Add paragraph">
<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="M14 17H4v2h10v-2zm6-8H4v2h16V9zM4 15h16v-2H4v2zM4 5v2h16V5H4z" />
</svg>
<span class="actionable-button__title">
Section
</span>
</button>
</div>
</div> -->
<aside id="version_history_panel" class="flex direction-column hide-completely"> <aside id="version_history_panel" class="flex direction-column hide-completely">
<div class="flex align-center space-between"> <div class="flex align-center space-between">
<div class="flex align-center"> <div class="flex align-center">
@ -178,18 +173,24 @@
</article> </article>
<sm-popup id="article_list_popup"> <sm-popup id="article_list_popup">
<header slot="header" class="popup__header"> <header slot="header" class="popup__header">
<div class="grid"> <h3>All articles</h3>
<div class="flex align-center space-between"> <sm-input id="article_list_search" placeholder="Search articles" type="search" autofocus>
<h3>All articles</h3> <svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
<sm-select label="Sort by:"> width="24px" fill="#000000">
<sm-option value="time">Date created</sm-option> <path d="M0 0h24v24H0z" fill="none" />
<sm-option value="az">A-Z</sm-option> <path
</sm-select> 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" />
</div> </svg>
<sm-input placeholder="Search articles" type="search"></sm-input> </sm-input>
</div> <sm-select id="sort_article_list" label="Sort by:">
<sm-option value="time">Most recent</sm-option>
<sm-option value="az">A-Z</sm-option>
</sm-select>
</header> </header>
<div id="article_list"></div> <div id="article_list" class="observe-empty-state grid"></div>
<h4 class="empty-state">
No related article
</h4>
</sm-popup> </sm-popup>
<sm-popup id="create_article_popup"> <sm-popup id="create_article_popup">
<header slot="header" class="popup__header"> <header slot="header" class="popup__header">
@ -204,7 +205,7 @@
<h3>Create article</h3> <h3>Create article</h3>
</header> </header>
<sm-form> <sm-form>
<sm-input placeholder="Article title" required></sm-input> <sm-input placeholder="Article title" required autofocus></sm-input>
<sm-checkbox checked> <sm-checkbox checked>
<div class="grid button__icon--right gap-0-5"> <div class="grid button__icon--right gap-0-5">
Set as default Set as default
@ -221,6 +222,44 @@
<sm-button variant="primary">Create</sm-button> <sm-button variant="primary">Create</sm-button>
</sm-form> </sm-form>
</sm-popup> </sm-popup>
<sm-popup id="edit_sections_popup" open>
<header slot="header" class="popup__header">
<button class="popup__header__close" onclick="hidePopup()">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
</svg>
</button>
<h3>Edit sections</h3>
</header>
<h5 class="label">Existing sections</h5>
<div id="section_list_container" class="observe-empty-state"></div>
<p class="empty-state">
There are no sections so far, you can add section with button below.
</p>
<button id="insert_section_button" onclick="insertEmptySection()" class="button">
<svg class="icon button__icon--left" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24"
height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
<g>
<rect fill="none" height="24" width="24" />
</g>
<g>
<g />
<g>
<path
d="M17,19.22H5V7h7V5H5C3.9,5,3,5.9,3,7v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-7h-2V19.22z" />
<path d="M19,2h-2v3h-3c0.01,0.01,0,2,0,2h3v2.99c0.01,0.01,2,0,2,0V7h3V5h-3V2z" />
<rect height="2" width="8" x="7" y="9" />
<polygon points="7,12 7,14 15,14 15,12 12,12" />
<rect height="2" width="8" x="7" y="15" />
</g>
</g>
</svg>
Add new section
</button>
</sm-popup>
<template id="section_template"> <template id="section_template">
<text-field class="heading"></text-field> <text-field class="heading"></text-field>
<section class="article-section"> <section class="article-section">
@ -263,10 +302,11 @@
<template id="quote_template"> <template id="quote_template">
<figure class="quote-template grid gap-0-5" contenteditable="false"> <figure class="quote-template grid gap-0-5" contenteditable="false">
<blockquote class="flex"> <blockquote class="flex">
<p class="quote" contenteditable="true"></p> <p class="quote" contenteditable="true">Words can be like X-rays, if you use them properly—theyll go
through anything. You read and youre pierced.</p>
</blockquote> </blockquote>
<figcaption class="flex"><span class="by" contenteditable="true"></span><cite class="citation" <figcaption class="flex"><span class="by" contenteditable="true">Aldous Huxley</span><cite class="citation"
contenteditable="true"></cite></figcaption> contenteditable="true">Brave New World</cite></figcaption>
</figure> </figure>
<p><br></p> <p><br></p>
</template> </template>
@ -504,7 +544,6 @@
window.addEventListener('hashchange', e => showPage(window.location.hash)) window.addEventListener('hashchange', e => showPage(window.location.hash))
window.addEventListener("load", () => { window.addEventListener("load", () => {
showPage(window.location.hash)
document.body.classList.remove('hide-completely') document.body.classList.remove('hide-completely')
document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex) document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex)
document.addEventListener('keyup', (e) => { document.addEventListener('keyup', (e) => {
@ -525,7 +564,8 @@
async function showPage(targetPage, options = {}) { async function showPage(targetPage, options = {}) {
const { firstLoad, hashChange } = options const { firstLoad, hashChange } = options
let pageId let pageId
let params let params = {}
let searchParams
if (targetPage === '') { if (targetPage === '') {
if (typeof myFloID === "undefined") { if (typeof myFloID === "undefined") {
pageId = 'landing' pageId = 'landing'
@ -549,8 +589,26 @@
pageId = targetPage pageId = targetPage
} }
} }
if (searchParams) {
const urlSearchParams = new URLSearchParams('?' + searchParams);
params = Object.fromEntries(urlSearchParams.entries());
pagesData.params = params
}
switch (pageId) {
case 'home':
if (!params.articleID)
params['articleID'] = floGlobals.appObjects.cc.defaultArticle
Promise.all([
floCloudAPI.requestObjectData(params.articleID),
floCloudAPI.requestGeneralData(`${params.articleID}_gd`)
]).then(() => {
hidePopup()
render.article(params.articleID)
window.history.replaceState('', '', `#/home?articleID=${params.articleID}`)
})
break
}
if (pagesData.lastPage !== pageId) { if (pagesData.lastPage !== pageId) {
document.querySelectorAll('.mobile-page').forEach(elem => elem.classList.add('hide-on-mobile'))
pagesData.lastPage = pageId pagesData.lastPage = pageId
if (!pagesData.openedPages.includes(pageId)) { if (!pagesData.openedPages.includes(pageId)) {
pagesData.openedPages.push(pageId) pagesData.openedPages.push(pageId)
@ -872,20 +930,23 @@
const { title } = floGlobals.appObjects.cc.articleList[id] const { title } = floGlobals.appObjects.cc.articleList[id]
const { writer, sections } = currentArticle const { writer, sections } = currentArticle
const frag = document.createDocumentFragment() const frag = document.createDocumentFragment()
const isSubAdmin = floGlobals.subAdmins.includes(myFloID)
let index = 0 let index = 0
for (const sectionID in sections) { for (const sectionID in sections) {
frag.append(render.section(sectionID, sections[sectionID], index)) frag.append(render.section(sectionID, sections[sectionID], index))
index += 1 index += 1
} }
if (!floGlobals.subAdmins.includes(myFloID)) { if (!isSubAdmin) {
getRef('current_article_title').setAttribute('disabled', '') getRef('current_article_title').setAttribute('disabled', '')
} else {
renderSectionList()
} }
getRef('current_article_title').value = title getRef('current_article_title').value = title
getRef('article_wrapper').innerHTML = '' getRef('article_wrapper').innerHTML = ''
getRef('article_wrapper').append(frag) getRef('article_wrapper').append(frag)
}, },
articleLink(uid, details, isDefaultArticle) { articleLink(details, isDefaultArticle) {
const { timestamp, title } = details const { uid, timestamp, title } = details
const link = createElement('a', { const link = createElement('a', {
textContent: title, textContent: title,
attributes: { href: `#/home?articleID=${uid}` }, attributes: { href: `#/home?articleID=${uid}` },
@ -894,7 +955,7 @@
if (isDefaultArticle) { if (isDefaultArticle) {
link.append(createElement('span', { link.append(createElement('span', {
className: 'default-article', className: 'default-article',
textContent: 'Active' textContent: 'Actively written'
})) }))
} }
return link return link
@ -933,7 +994,7 @@
} }
if (i - index > 1) if (i - index > 1)
changed.splice((index + 1), (i - index - 1)) changed.splice((index + 1), (i - index - 1))
return `<span class="${type ? 'added' : 'removed'}">${consecutiveWords.join(' ')}</span>` return `<s${type ? 'pan' : ''} class="${type ? 'added' : 'removed'}">${consecutiveWords.join(' ')}</s${type ? 'pan' : ''}>`
} else return word } else return word
}) })
clone.querySelector('.entry__changes').innerHTML = DOMPurify.sanitize(final.join(' ')) clone.querySelector('.entry__changes').innerHTML = DOMPurify.sanitize(final.join(' '))
@ -959,6 +1020,16 @@
section.querySelector('.article-section').append(frag) section.querySelector('.article-section').append(frag)
return section return section
}, },
sectionCard(details) {
const { title } = details
return createElement('div', {
className: 'section-card flex align-center',
innerHTML: `
<svg class="icon button__icon--left" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M9,18h12v-2H9V18z M3,6v2h18V6H3z M9,13h12v-2H9V13z"/></g></svg>
${title}
`
})
}
} }
function parseArticleData() { function parseArticleData() {
@ -994,18 +1065,74 @@
currentArticle.uniqueEntries[entry]['iterations'].sort((a, b) => a.timestamp - b.timestamp) currentArticle.uniqueEntries[entry]['iterations'].sort((a, b) => a.timestamp - b.timestamp)
} }
} }
function renderSectionList() {
const frag = document.createDocumentFragment()
floGlobals.appObjects[currentArticle.id].sections.forEach(section => {
frag.append(render.sectionCard(section))
})
getRef('section_list_container').innerHTML = ''
getRef('section_list_container').append(frag)
}
function insertEmptySection() {
const emptySection = createElement('div', {
className: 'section-card section-card--new flex align-center',
innerHTML: `
<svg class="icon button__icon--left" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M9,18h12v-2H9V18z M3,6v2h18V6H3z M9,13h12v-2H9V13z"/></g></svg>
<input placeholder="New section title"/>
<button class="remove">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none"></path>
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"></path>
</svg>
</button>
`
})
getRef('section_list_container').append(emptySection)
}
function renderArticleList() { function getArticleList() {
const articleList = floGlobals.appObjects.cc.articleList
const sortBy = getRef('sort_article_list').value
const arrayOfObject = []
for (const key in articleList) {
const { timestamp, title } = articleList[key]
arrayOfObject.push({
uid: key,
timestamp,
title
})
}
if (sortBy === 'az') {
arrayOfObject.sort((a, b) => a.title.localeCompare(b.title, 'en', { numeric: true, sensitivity: 'base' }))
} else {
arrayOfObject.sort((a, b) => b.timestamp - a.timestamp)
}
return arrayOfObject
}
getRef('article_list_search').addEventListener('input', e => {
const searchKey = e.target.value.trim()
const options = {
keys: ['title'],
threshold: 0
}
const fuse = new Fuse(getArticleList(), options)
renderArticleList(searchKey === '' ? undefined : fuse.search(searchKey).map(v => v.item))
})
getRef('sort_article_list').addEventListener('change', e => { renderArticleList() })
function renderArticleList(articleList) {
getRef('article_list').innerHTML = `` getRef('article_list').innerHTML = ``
const frag = document.createDocumentFragment() const frag = document.createDocumentFragment()
const { articleList, defaultArticle } = floGlobals.appObjects.cc const defaultArticle = floGlobals.appObjects.cc.defaultArticle;
for (articleKey in articleList) { (articleList || getArticleList()).forEach((article) => {
const isDefaultArticle = defaultArticle === articleKey const isDefaultArticle = defaultArticle === article.uid
frag.prepend(render.articleLink(articleKey, articleList[articleKey], isDefaultArticle)) frag.append(render.articleLink(article, isDefaultArticle))
} })
getRef('article_list').append(frag) getRef('article_list').append(frag)
} }
function getIterationDetails(uid, targetIndex) { function getIterationDetails(uid, targetIndex) {
let merged let merged
const contributors = new Set() const contributors = new Set()
@ -1111,7 +1238,7 @@
`${origin.substring(0, startIndex)}${insertion}${origin.substring(endIndex)}`; `${origin.substring(0, startIndex)}${insertion}${origin.substring(endIndex)}`;
const make = { const make = {
bold() { bold() {
document.execCommand('bold') replaceSelectedText(createElement('strong'))
}, },
italic() { italic() {
document.execCommand('italic') document.execCommand('italic')
@ -1221,7 +1348,7 @@
function createQuote() { function createQuote() {
const quote = getRef('quote_template').content.cloneNode(true) const quote = getRef('quote_template').content.cloneNode(true)
quote.querySelector('.quote').textContent = '"Words can be like X-rays, if you use them properly—theyll go through anything. You read and youre pierced."' quote.querySelector('.quote').textContent = '"Words can be like X-rays, if you use them properly—theyll go through anything. You read and youre pierced."'
quote.querySelector('.by').textContent = `- Author` quote.querySelector('.by').textContent = `- Aldous Huxley,`
quote.querySelector('.citation').textContent = 'Brave New World' quote.querySelector('.citation').textContent = 'Brave New World'
return quote return quote
} }
@ -1240,14 +1367,8 @@
await Promise.all([ await Promise.all([
floCloudAPI.requestObjectData('cc'), floCloudAPI.requestObjectData('cc'),
]) ])
Promise.all([ showPage(window.location.hash)
floCloudAPI.requestObjectData(floGlobals.appObjects.cc.defaultArticle), renderArticleList()
floCloudAPI.requestGeneralData(`${floGlobals.appObjects.cc.defaultArticle}_gd`)
])
.then(() => {
render.article(floGlobals.appObjects.cc.defaultArticle)
renderArticleList()
})
console.log(result) console.log(result)
// alert(`Welcome FLO_ID: ${ myFloID }`) // alert(`Welcome FLO_ID: ${ myFloID }`)