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;
--min-width: auto;
--min-height: auto;
--body-padding: 1.5rem;
--backdrop-background: rgba(0, 0, 0, 0.6);
--border-radius: 0.8rem 0.8rem 0 0;
}
@ -1087,13 +1086,13 @@ smPopup.innerHTML = `
-ms-flex: 1;
flex: 1;
width: 100%;
padding: var(--body-padding);
padding: var(--body-padding, 1.5rem);
overflow-y: auto;
}
.hide{
opacity: 0;
pointer-events: none;
visiblity: none;
visibility: none;
}
@media screen and (min-width: 640px){
:host{
@ -1169,7 +1168,10 @@ customElements.define('sm-popup', class extends HTMLElement {
this.touchEndY = 0;
this.touchStartTime = 0;
this.touchEndTime = 0;
this.touchEndAnimataion = undefined;
this.touchEndAnimation = undefined;
this.focusable
this.autoFocus
this.mutationObserver
this.popupContainer = this.shadowRoot.querySelector('.popup-container');
this.popup = this.shadowRoot.querySelector('.popup');
@ -1183,6 +1185,7 @@ customElements.define('sm-popup', class extends HTMLElement {
this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
this.movePopup = this.movePopup.bind(this);
this.detectFocus = this.detectFocus.bind(this);
}
static get observedAttributes() {
@ -1231,6 +1234,8 @@ customElements.define('sm-popup', class extends HTMLElement {
this.popup.style.transform = 'none';
document.body.style.overflow = 'hidden';
document.body.style.top = `-${window.scrollY}px`;
const elementToFocus = this.autoFocus || this.focusable[0];
elementToFocus.tagName.includes('SM-') ? elementToFocus.focusIn() : elementToFocus.focus();
return this.popupStack;
}
hide() {
@ -1279,13 +1284,13 @@ customElements.define('sm-popup', class extends HTMLElement {
handleTouchMove(e) {
if (this.touchStartY < e.changedTouches[0].clientY) {
this.offset = e.changedTouches[0].clientY - this.touchStartY;
this.touchEndAnimataion = window.requestAnimationFrame(() => this.movePopup());
this.touchEndAnimation = window.requestAnimationFrame(() => this.movePopup());
}
}
handleTouchEnd(e) {
this.touchEndTime = e.timeStamp;
cancelAnimationFrame(this.touchEndAnimataion);
cancelAnimationFrame(this.touchEndAnimation);
this.touchEndY = e.changedTouches[0].clientY;
this.popup.style.transition = 'transform 0.3s';
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)`;
}
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() {
this.popupBodySlot.addEventListener('slotchange', () => {
this.forms = this.querySelectorAll('sm-form');
@ -1340,16 +1359,26 @@ customElements.define('sm-popup', class extends HTMLElement {
});
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('touchmove', this.handleTouchMove, { passive: true });
this.popupHeader.addEventListener('touchend', this.handleTouchEnd, { passive: true });
}
disconnectedCallback() {
this.removeEventListener('keydown', this.detectFocus);
this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, { passive: true });
this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, { passive: true });
resizeObserver.unobserve();
this.mutationObserver.disconnect()
}
attributeChangedCallback(name) {
if (name === 'open') {
@ -2329,7 +2358,7 @@ smSelect.innerHTML = `
}
}
</style>
<div class="select" >
<div class="select">
<div class="selection">
<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>
@ -2345,6 +2374,7 @@ customElements.define('sm-select', class extends HTMLElement {
mode: 'open'
}).append(smSelect.content.cloneNode(true))
this.focusIn = this.focusIn.bind(this)
this.reset = this.reset.bind(this)
this.open = this.open.bind(this)
this.collapse = this.collapse.bind(this)
@ -2413,6 +2443,10 @@ customElements.define('sm-select', class extends HTMLElement {
}
}
focusIn() {
this.selection.focus()
}
open() {
this.optionList.classList.remove('hide')
this.optionList.animate(this.slideDown, this.animationOptions)
@ -2757,6 +2791,10 @@ customElements.define('sm-checkbox', class extends HTMLElement {
return this.getAttribute('value')
}
focusIn() {
this.focus()
}
reset() {
this.removeAttribute('checked')
}
@ -3447,7 +3485,6 @@ textField.innerHTML = `
align-items: center;
}
.text{
padding: 0.6rem 0;
transition: background-color 0.3s;
border-bottom: 0.15rem solid transparent;
overflow-wrap: break-word;
@ -3657,4 +3694,314 @@ customElements.define('text-field', class extends HTMLElement {
this.editButton.removeEventListener('click', this.setEditable)
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 * {
--accent-color: #504dff;
--accent-color--light: #eeeeff;
--accent-color: rgb(0, 156, 78);
--text-color: 36, 36, 36;
--background-color: 248, 248, 248;
--foreground-color: rgb(255, 255, 255);
@ -36,8 +35,7 @@ body * {
body[data-theme=dark],
body[data-theme=dark] * {
--accent-color: #a3a1ff;
--accent-color--light: rgba(142, 140, 255, 0.06);
--accent-color: rgb(14, 230, 122);
--text-color: 230, 230, 230;
--text-color-light: 170, 170, 170;
--background-color: 10, 10, 10;
@ -501,6 +499,14 @@ sm-checkbox {
-webkit-tap-highlight-color: transparent;
}
sm-menu {
--background: var(--foreground-color);
}
menu-option {
font-size: 0.9rem;
}
.warning {
background-color: khaki;
color: rgba(0, 0, 0, 0.7);
@ -524,11 +530,11 @@ sm-checkbox {
#main_header {
display: grid;
gap: 1rem;
padding: 1rem 1.5rem;
padding: 1rem;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
grid-template-columns: auto 1fr auto auto;
grid-template-columns: 1fr auto auto;
grid-column: 1/-1;
background-color: var(--foreground-color);
}
@ -556,6 +562,24 @@ sm-checkbox {
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 {
padding: 1rem;
color: rgba(var(--text-color), 0.8);
@ -577,6 +601,40 @@ sm-checkbox {
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 {
display: -webkit-box;
display: -ms-flexbox;
@ -608,7 +666,7 @@ sm-checkbox {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
gap: 0.5rem;
gap: 1rem;
overflow-x: auto;
-ms-flex-negative: 0;
flex-shrink: 0;
@ -623,7 +681,7 @@ sm-checkbox {
}
.content-card {
scroll-snap-align: center;
scroll-snap-align: start;
width: min(46ch, 100%);
-ms-flex-negative: 0;
flex-shrink: 0;
@ -806,7 +864,8 @@ sm-checkbox {
background-color: #00e67650;
}
.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) {
@ -819,6 +878,17 @@ sm-checkbox {
--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 {
display: none;
}
@ -861,6 +931,11 @@ sm-checkbox {
display: none;
}
#main_header {
padding: 1rem 1.5rem;
grid-template-columns: auto 1fr auto auto;
}
#main_page.active-sidebar {
height: 100%;
overflow-y: hidden;
@ -871,6 +946,15 @@ sm-checkbox {
height: 100%;
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) {
::-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 {
&,
* {
--accent-color: #504dff;
--accent-color--light: #eeeeff;
--accent-color: rgb(0, 156, 78);
--text-color: 36, 36, 36;
--background-color: 248, 248, 248;
--foreground-color: rgb(255, 255, 255);
@ -37,8 +36,7 @@ body {
body[data-theme="dark"] {
&,
* {
--accent-color: #a3a1ff;
--accent-color--light: rgba(142, 140, 255, 0.06);
--accent-color: rgb(14, 230, 122);
--text-color: 230, 230, 230;
--text-color-light: 170, 170, 170;
--background-color: 10, 10, 10;
@ -440,6 +438,12 @@ sm-checkbox {
--width: 1rem;
-webkit-tap-highlight-color: transparent;
}
sm-menu {
--background: var(--foreground-color);
}
menu-option {
font-size: 0.9rem;
}
.warning {
background-color: khaki;
color: rgba(0, 0, 0, 0.7);
@ -461,9 +465,9 @@ sm-checkbox {
#main_header {
display: grid;
gap: 1rem;
padding: 1rem 1.5rem;
padding: 1rem;
align-items: center;
grid-template-columns: auto 1fr auto auto;
grid-template-columns: 1fr auto auto;
grid-column: 1/-1;
background-color: var(--foreground-color);
}
@ -489,6 +493,24 @@ sm-checkbox {
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 {
padding: 1rem;
color: rgba(var(--text-color), 0.8);
@ -509,6 +531,38 @@ sm-checkbox {
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 {
display: flex;
flex-direction: column;
@ -531,7 +585,7 @@ sm-checkbox {
.article-section {
display: flex;
gap: 0.5rem;
gap: 1rem;
overflow-x: auto;
flex-shrink: 0;
scroll-snap-type: x mandatory;
@ -543,7 +597,7 @@ sm-checkbox {
}
}
.content-card {
scroll-snap-align: center;
scroll-snap-align: start;
width: min(46ch, 100%);
flex-shrink: 0;
border-radius: 0.5rem;
@ -704,7 +758,7 @@ sm-checkbox {
background-color: #00e67650;
}
.removed {
background-color: #ff3a4a50;
text-decoration-color: red;
}
}
@ -719,6 +773,14 @@ sm-checkbox {
sm-button {
--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 {
display: none;
}
@ -755,6 +817,10 @@ sm-checkbox {
.hide-on-desktop {
display: none;
}
#main_header {
padding: 1rem 1.5rem;
grid-template-columns: auto 1fr auto auto;
}
#main_page {
&.active-sidebar {
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) {
::-webkit-scrollbar {

View File

@ -12,6 +12,7 @@
<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">
<script src="purify.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.4.6" defer></script>
<script id="floGlobals">
/* Constants for FLO blockchain operations !!Make sure to add this at begining!! */
const floGlobals = {
@ -71,23 +72,36 @@
<h4>Content Collab</h4>
</div>
</div>
<div id="article_name_wrapper" class="flex align-center justify-center">
<button title="Create new article" class="button__icon--left"
onclick="showPopup('create_article_popup')">
<div id="article_name_wrapper" class="flex gap-0-5 align-center justify-center">
<button title="Show all articles list" onclick="showPopup('article_list_popup')">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="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>
</button>
<text-field id="current_article_title"></text-field>
<button class="button__icon--right" onclick="showPopup('article_list_popup')">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
fill="#000000">
<path d="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>
<sm-menu align-options="right">
<menu-option onclick="showPopup('create_article_popup')">
<svg class="icon button__icon--left" xmlns="http://www.w3.org/2000/svg" height="24px"
viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg>
Create new article
</menu-option>
<menu-option onclick="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>
<theme-toggle></theme-toggle>
<button onclick="showPopup('user_popup')">
@ -134,25 +148,6 @@
</button>
</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">
<div class="flex align-center space-between">
<div class="flex align-center">
@ -178,18 +173,24 @@
</article>
<sm-popup id="article_list_popup">
<header slot="header" class="popup__header">
<div class="grid">
<div class="flex align-center space-between">
<h3>All articles</h3>
<sm-select label="Sort by:">
<sm-option value="time">Date created</sm-option>
<sm-option value="az">A-Z</sm-option>
</sm-select>
</div>
<sm-input placeholder="Search articles" type="search"></sm-input>
</div>
<h3>All articles</h3>
<sm-input id="article_list_search" placeholder="Search articles" type="search" autofocus>
<svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
width="24px" fill="#000000">
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
</svg>
</sm-input>
<sm-select id="sort_article_list" label="Sort by:">
<sm-option value="time">Most recent</sm-option>
<sm-option value="az">A-Z</sm-option>
</sm-select>
</header>
<div id="article_list"></div>
<div id="article_list" class="observe-empty-state grid"></div>
<h4 class="empty-state">
No related article
</h4>
</sm-popup>
<sm-popup id="create_article_popup">
<header slot="header" class="popup__header">
@ -204,7 +205,7 @@
<h3>Create article</h3>
</header>
<sm-form>
<sm-input placeholder="Article title" required></sm-input>
<sm-input placeholder="Article title" required autofocus></sm-input>
<sm-checkbox checked>
<div class="grid button__icon--right gap-0-5">
Set as default
@ -221,6 +222,44 @@
<sm-button variant="primary">Create</sm-button>
</sm-form>
</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">
<text-field class="heading"></text-field>
<section class="article-section">
@ -263,10 +302,11 @@
<template id="quote_template">
<figure class="quote-template grid gap-0-5" contenteditable="false">
<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>
<figcaption class="flex"><span class="by" contenteditable="true"></span><cite class="citation"
contenteditable="true"></cite></figcaption>
<figcaption class="flex"><span class="by" contenteditable="true">Aldous Huxley</span><cite class="citation"
contenteditable="true">Brave New World</cite></figcaption>
</figure>
<p><br></p>
</template>
@ -504,7 +544,6 @@
window.addEventListener('hashchange', e => showPage(window.location.hash))
window.addEventListener("load", () => {
showPage(window.location.hash)
document.body.classList.remove('hide-completely')
document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = floCrypto.getPubKeyHex)
document.addEventListener('keyup', (e) => {
@ -525,7 +564,8 @@
async function showPage(targetPage, options = {}) {
const { firstLoad, hashChange } = options
let pageId
let params
let params = {}
let searchParams
if (targetPage === '') {
if (typeof myFloID === "undefined") {
pageId = 'landing'
@ -549,8 +589,26 @@
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) {
document.querySelectorAll('.mobile-page').forEach(elem => elem.classList.add('hide-on-mobile'))
pagesData.lastPage = pageId
if (!pagesData.openedPages.includes(pageId)) {
pagesData.openedPages.push(pageId)
@ -872,20 +930,23 @@
const { title } = floGlobals.appObjects.cc.articleList[id]
const { writer, sections } = currentArticle
const frag = document.createDocumentFragment()
const isSubAdmin = floGlobals.subAdmins.includes(myFloID)
let index = 0
for (const sectionID in sections) {
frag.append(render.section(sectionID, sections[sectionID], index))
index += 1
}
if (!floGlobals.subAdmins.includes(myFloID)) {
if (!isSubAdmin) {
getRef('current_article_title').setAttribute('disabled', '')
} else {
renderSectionList()
}
getRef('current_article_title').value = title
getRef('article_wrapper').innerHTML = ''
getRef('article_wrapper').append(frag)
},
articleLink(uid, details, isDefaultArticle) {
const { timestamp, title } = details
articleLink(details, isDefaultArticle) {
const { uid, timestamp, title } = details
const link = createElement('a', {
textContent: title,
attributes: { href: `#/home?articleID=${uid}` },
@ -894,7 +955,7 @@
if (isDefaultArticle) {
link.append(createElement('span', {
className: 'default-article',
textContent: 'Active'
textContent: 'Actively written'
}))
}
return link
@ -933,7 +994,7 @@
}
if (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
})
clone.querySelector('.entry__changes').innerHTML = DOMPurify.sanitize(final.join(' '))
@ -959,6 +1020,16 @@
section.querySelector('.article-section').append(frag)
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() {
@ -994,18 +1065,74 @@
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 = ``
const frag = document.createDocumentFragment()
const { articleList, defaultArticle } = floGlobals.appObjects.cc
for (articleKey in articleList) {
const isDefaultArticle = defaultArticle === articleKey
frag.prepend(render.articleLink(articleKey, articleList[articleKey], isDefaultArticle))
}
const defaultArticle = floGlobals.appObjects.cc.defaultArticle;
(articleList || getArticleList()).forEach((article) => {
const isDefaultArticle = defaultArticle === article.uid
frag.append(render.articleLink(article, isDefaultArticle))
})
getRef('article_list').append(frag)
}
function getIterationDetails(uid, targetIndex) {
let merged
const contributors = new Set()
@ -1111,7 +1238,7 @@
`${origin.substring(0, startIndex)}${insertion}${origin.substring(endIndex)}`;
const make = {
bold() {
document.execCommand('bold')
replaceSelectedText(createElement('strong'))
},
italic() {
document.execCommand('italic')
@ -1221,7 +1348,7 @@
function createQuote() {
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('.by').textContent = `- Author`
quote.querySelector('.by').textContent = `- Aldous Huxley,`
quote.querySelector('.citation').textContent = 'Brave New World'
return quote
}
@ -1240,14 +1367,8 @@
await Promise.all([
floCloudAPI.requestObjectData('cc'),
])
Promise.all([
floCloudAPI.requestObjectData(floGlobals.appObjects.cc.defaultArticle),
floCloudAPI.requestGeneralData(`${floGlobals.appObjects.cc.defaultArticle}_gd`)
])
.then(() => {
render.article(floGlobals.appObjects.cc.defaultArticle)
renderArticleList()
})
showPage(window.location.hash)
renderArticleList()
console.log(result)
// alert(`Welcome FLO_ID: ${ myFloID }`)