Feature update

- added option to mark any open article as default
- added new article creation UI and flow
- added feature to add section after creation of article
This commit is contained in:
sairaj mote 2021-11-26 01:49:15 +05:30
parent 70b13b7f5a
commit 0bee787ccb
5 changed files with 652 additions and 222 deletions

View File

@ -74,11 +74,13 @@ smButton.innerHTML = `
background-color: rgba(var(--text-color), 0.3);
}
@media (hover: hover){
:host(:not([disabled])) .button:hover{
:host(:not([disabled])) .button:hover,
:host(:focus-within:not([disabled])) .button{
-webkit-box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.2rem 0.8rem rgba(0, 0, 0, 0.12);
box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.2rem 0.8rem rgba(0, 0, 0, 0.12);
box-shadow: 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.2rem 0.8rem rgba(0, 0, 0, 0.12);
}
:host([variant='outlined']) .button:hover{
:host([variant='outlined']:not([disabled])) .button:hover,
:host(:focus-within[variant='outlined']:not([disabled])) .button:hover{
-webkit-box-shadow: 0 0 0 1px rgba(var(--text-color), 0.2) inset, 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.4rem 0.8rem rgba(0, 0, 0, 0.12);
box-shadow: 0 0 0 1px rgba(var(--text-color), 0.2) inset, 0 0.1rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.4rem 0.8rem rgba(0, 0, 0, 0.12);
}
@ -120,6 +122,9 @@ customElements.define('sm-button',
this.removeAttribute('disabled');
}
}
focusIn() {
this.focus();
}
handleKeyDown(e) {
if (!this.hasAttribute('disabled') && (e.key === 'Enter' || e.code === 'Space')) {
@ -156,12 +161,11 @@ smForm.innerHTML = `
}
:host{
display: flex;
--gap: 1rem;
width: 100%;
}
form{
display: grid;
gap: var(--gap);
gap: var(--gap, 1.5rem);
width: 100%;
}
</style>
@ -986,6 +990,27 @@ customElements.define('sm-notifications', class extends HTMLElement {
});
}
});
class Stack {
constructor() {
this.items = [];
}
push(element) {
this.items.push(element);
}
pop() {
if (this.items.length == 0)
return "Underflow";
return this.items.pop();
}
peek() {
return this.items[this.items.length - 1];
}
}
const popupStack = new Stack();
const smPopup = document.createElement('template');
smPopup.innerHTML = `
<style>
@ -1019,10 +1044,6 @@ smPopup.innerHTML = `
left: 0;
right: 0;
place-items: center;
background: var(--backdrop-background);
-webkit-transition: opacity 0.3s;
-o-transition: opacity 0.3s;
transition: opacity 0.3s;
z-index: 10;
touch-action: none;
}
@ -1030,6 +1051,18 @@ smPopup.innerHTML = `
-webkit-transform: scale(0.9) translateY(-2rem) !important;
transform: scale(0.9) translateY(-2rem) !important;
}
.background{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: none;
background: var(--backdrop-background);
-webkit-transition: opacity 0.3s;
-o-transition: opacity 0.3s;
transition: opacity 0.3s;
}
.popup{
display: -webkit-box;
display: -ms-flexbox;
@ -1049,17 +1082,10 @@ smPopup.innerHTML = `
min-height: var(--min-height);
max-height: 90vh;
border-radius: var(--border-radius);
-webkit-transform: scale(1) translateY(100%);
transform: scale(1) translateY(100%);
-webkit-transition: -webkit-transform 0.3s;
transition: -webkit-transform 0.3s;
-o-transition: transform 0.3s;
transition: transform 0.3s, -webkit-transform 0.3s;
transition: transform 0.3s;
background: rgba(var(--background-color), 1);
-webkit-box-shadow: 0 -1rem 2rem #00000020;
box-shadow: 0 -1rem 2rem #00000020;
content-visibility: auto;
transition: transform 0.3s;
}
.container-header{
display: -webkit-box;
@ -1090,13 +1116,11 @@ smPopup.innerHTML = `
overflow-y: auto;
}
.hide{
opacity: 0;
pointer-events: none;
visibility: none;
display:none;
}
@media screen and (min-width: 640px){
:host{
--border-radius: 0.4rem;
--border-radius: 0.5rem;
}
.popup{
-ms-flex-item-align: center;
@ -1104,8 +1128,6 @@ smPopup.innerHTML = `
align-self: center;
border-radius: var(--border-radius);
height: var(--height);
-webkit-transform: scale(1) translateY(3rem);
transform: scale(1) translateY(3rem);
-webkit-box-shadow: 0 3rem 2rem -0.5rem #00000040;
box-shadow: 0 3rem 2rem -0.5rem #00000040;
}
@ -1140,7 +1162,8 @@ smPopup.innerHTML = `
}
}
</style>
<div part="background" class="popup-container hide" role="dialog">
<div class="popup-container hide" role="dialog">
<div part="background" class="background"></div>
<div part="popup" class="popup">
<div part="popup-header" class="popup-top">
<div class="handle"></div>
@ -1162,7 +1185,6 @@ customElements.define('sm-popup', class extends HTMLElement {
this.allowClosing = false;
this.isOpen = false;
this.pinned = false;
this.popupStack = undefined;
this.offset = 0;
this.touchStartY = 0;
this.touchEndY = 0;
@ -1174,17 +1196,18 @@ customElements.define('sm-popup', class extends HTMLElement {
this.mutationObserver
this.popupContainer = this.shadowRoot.querySelector('.popup-container');
this.backdrop = this.shadowRoot.querySelector('.background');
this.popup = this.shadowRoot.querySelector('.popup');
this.popupBodySlot = this.shadowRoot.querySelector('.popup-body slot');
this.popupHeader = this.shadowRoot.querySelector('.popup-top');
this.resumeScrolling = this.resumeScrolling.bind(this);
this.setStateOpen = this.setStateOpen.bind(this);
this.show = this.show.bind(this);
this.hide = this.hide.bind(this);
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
this.movePopup = this.movePopup.bind(this);
this.detectFocus = this.detectFocus.bind(this);
}
@ -1196,86 +1219,140 @@ customElements.define('sm-popup', class extends HTMLElement {
return this.isOpen;
}
animateTo(element, keyframes, options) {
const anime = element.animate(keyframes, { ...options, fill: 'both' })
anime.finished.then(() => {
anime.commitStyles()
anime.cancel()
})
return anime
}
resumeScrolling() {
const scrollY = document.body.style.top;
window.scrollTo(0, parseInt(scrollY || '0') * -1);
setTimeout(() => {
document.body.style.overflow = 'auto';
document.body.style.top = 'initial';
}, 300);
document.body.style.overflow = 'auto';
document.body.style.top = 'initial';
}
setStateOpen() {
const animOptions = {
duration: 300,
easing: 'ease'
}
const initialAnimation = (window.innerWidth > 640) ? 'scale(1.1)' : `translateY(${this.offset ? `${this.offset}px` : '100%'})`
this.animateTo(this.popup, [
{
opacity: this.offset ? 1 : 0,
transform: initialAnimation
},
{
opacity: 1,
transform: 'none'
},
], animOptions)
}
show(options = {}) {
const { pinned = false, popupStack } = options;
if (popupStack)
this.popupStack = popupStack;
if (this.popupStack && !this.hasAttribute('open')) {
this.popupStack.push({
popup: this,
permission: pinned
});
if (this.popupStack.items.length > 1) {
this.popupStack.items[this.popupStack.items.length - 2].popup.classList.add('stacked');
const { pinned = false } = options;
if (!this.isOpen) {
const animOptions = {
duration: 300,
easing: 'ease'
}
if (popupStack) {
popupStack.push({
popup: this,
permission: pinned
});
if (popupStack.items.length > 1) {
this.animateTo(popupStack.items[popupStack.items.length - 2].popup.shadowRoot.querySelector('.popup'), [
{ transform: 'none' },
{ transform: 'translateY(-1.5rem) scale(0.9)' },
], animOptions)
}
}
this.popupContainer.classList.remove('hide');
if (!this.offset)
this.backdrop.animate([
{ opacity: 0 },
{ opacity: 1 },
], animOptions)
this.setStateOpen()
this.dispatchEvent(
new CustomEvent("popupopened", {
bubbles: true,
detail: {
popup: this,
popupStack: this.popupStack
}
})
);
this.setAttribute('open', '');
this.pinned = pinned;
this.isOpen = true;
document.body.style.overflow = 'hidden';
document.body.style.top = `-${window.scrollY}px`;
const elementToFocus = this.autoFocus || this.focusable[0];
elementToFocus.tagName.includes('SM-') ? elementToFocus.focusIn() : elementToFocus.focus();
if (!this.hasAttribute('open'))
this.setAttribute('open', '');
}
this.popupContainer.classList.remove('hide');
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() {
if (window.innerWidth < 640)
this.popup.style.transform = 'translateY(100%)';
else
this.popup.style.transform = 'translateY(3rem)';
this.popupContainer.classList.add('hide');
this.removeAttribute('open');
if (typeof this.popupStack !== 'undefined') {
this.popupStack.pop();
if (this.popupStack.items.length) {
this.popupStack.items[this.popupStack.items.length - 1].popup.classList.remove('stacked');
} else {
this.resumeScrolling();
}
} else {
this.resumeScrolling();
const animOptions = {
duration: 150,
easing: 'ease'
}
this.backdrop.animate([
{ opacity: 1 },
{ opacity: 0 }
], animOptions)
this.animateTo(this.popup, [
{
opacity: 1,
transform: (window.innerWidth > 640) ? 'none' : `translateY(${this.offset ? `${this.offset}px` : '0'})`
},
{
opacity: 0,
transform: (window.innerWidth > 640) ? 'scale(1.1)' : 'translateY(100%)'
},
], animOptions).finished
.finally(() => {
this.popupContainer.classList.add('hide');
this.popup.style = ''
this.removeAttribute('open');
if (typeof popupStack !== 'undefined') {
popupStack.pop();
if (popupStack.items.length) {
this.animateTo(popupStack.items[popupStack.items.length - 1].popup.shadowRoot.querySelector('.popup'), [
{ transform: 'translateY(-1.5rem) scale(0.9)' },
{ transform: 'none' },
], animOptions)
if (this.forms.length) {
setTimeout(() => {
this.forms.forEach(form => form.reset());
}, 300);
}
setTimeout(() => {
this.dispatchEvent(
new CustomEvent("popupclosed", {
bubbles: true,
detail: {
popup: this,
popupStack: this.popupStack
} else {
this.resumeScrolling();
}
})
);
this.isOpen = false;
}, 300);
} else {
this.resumeScrolling();
}
if (this.forms.length) {
this.forms.forEach(form => form.reset());
}
this.dispatchEvent(
new CustomEvent("popupclosed", {
bubbles: true,
detail: {
popup: this,
}
})
);
this.isOpen = false;
})
}
handleTouchStart(e) {
this.popupHeader.addEventListener('touchmove', this.handleTouchMove, { passive: true });
this.popupHeader.addEventListener('touchend', this.handleTouchEnd, { passive: true });
this.touchStartY = e.changedTouches[0].clientY;
this.popup.style.transition = 'transform 0.1s';
this.touchStartTime = e.timeStamp;
@ -1284,7 +1361,9 @@ 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.touchEndAnimation = window.requestAnimationFrame(() => this.movePopup());
this.touchEndAnimation = window.requestAnimationFrame(() => {
this.popup.style.transform = `translateY(${this.offset}px)`;
});
}
}
@ -1297,27 +1376,26 @@ customElements.define('sm-popup', class extends HTMLElement {
if (this.touchEndTime - this.touchStartTime > 200) {
if (this.touchEndY - this.touchStartY > this.threshold) {
if (this.pinned) {
this.show();
this.setStateOpen();
return;
} else
this.hide();
} else {
this.show();
this.setStateOpen();
}
} else {
if (this.touchEndY > this.touchStartY)
if (this.pinned) {
this.show();
this.setStateOpen();
return;
}
else
this.hide();
}
this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, { passive: true });
this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, { passive: true });
}
movePopup() {
this.popup.style.transform = `translateY(${this.offset}px)`;
}
detectFocus(e) {
if (e.code === 'Tab') {
@ -1333,14 +1411,20 @@ customElements.define('sm-popup', class extends HTMLElement {
}
}
updateFocusableList() {
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]')
}
connectedCallback() {
this.popupBodySlot.addEventListener('slotchange', () => {
this.forms = this.querySelectorAll('sm-form');
this.updateFocusableList()
});
this.popupContainer.addEventListener('mousedown', e => {
if (e.target === this.popupContainer && !this.pinned) {
if (this.pinned) {
this.show();
this.setStateOpen();
} else
this.hide();
}
@ -1360,25 +1444,18 @@ 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.updateFocusableList()
})
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()
this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
}
attributeChangedCallback(name) {
if (name === 'open') {
@ -2281,7 +2358,7 @@ smSelect.innerHTML = `
fill: rgba(var(--text-color), 0.7);
}
.selected-option-text{
font-size: 0.9rem;
font-size: inherit;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
@ -2315,6 +2392,7 @@ smSelect.innerHTML = `
}
.options{
top: 100%;
padding: var(--options-padding, 0.3rem);
margin-top: 0.2rem;
overflow: hidden auto;
position: absolute;
@ -2330,7 +2408,7 @@ smSelect.innerHTML = `
max-height: var(--max-height);
background: rgba(var(--background-color), 1);
border: solid 1px rgba(var(--text-color), 0.2);
border-radius: 0.3rem;
border-radius: var(--border-radius, 0.5rem);
z-index: 2;
-webkit-box-shadow: 0.4rem 0.8rem 1.2rem #00000030;
box-shadow: 0.4rem 0.8rem 1.2rem #00000030;
@ -2600,11 +2678,12 @@ smOption.innerHTML = `
width: 100%;
gap: 0.5rem;
grid-template-columns: max-content minmax(0, 1fr);
padding: 0.8rem 1.2rem;
padding: var(--padding, 0.6rem 1rem);
cursor: pointer;
white-space: nowrap;
outline: none;
user-select: none;
border-radius: var(--border-radius, 0.3rem);
}
:host(:focus){
outline: none;
@ -2746,6 +2825,7 @@ customElements.define('sm-checkbox', class extends HTMLElement {
mode: 'open'
}).append(smCheckbox.content.cloneNode(true))
this.defaultState
this.checkbox = this.shadowRoot.querySelector('.checkbox');
this.reset = this.reset.bind(this)
@ -2796,7 +2876,7 @@ customElements.define('sm-checkbox', class extends HTMLElement {
}
reset() {
this.removeAttribute('checked')
this.value = this.defaultState
}
dispatch() {
@ -2820,6 +2900,7 @@ customElements.define('sm-checkbox', class extends HTMLElement {
this.setAttribute('tabindex', '0')
}
this.setAttribute('role', 'checkbox')
this.defaultState = this.hasAttribute('checked')
if (!this.hasAttribute('checked')) {
this.setAttribute('aria-checked', 'false')
}
@ -3417,6 +3498,10 @@ customElements.define('sm-switch', class extends HTMLElement {
}
}
reset() {
}
dispatch() {
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
@ -4004,4 +4089,204 @@ customElements.define('menu-option', class extends HTMLElement {
}
})
}
})
})
const smTextarea = document.createElement('template')
smTextarea.innerHTML = `
<style>
*,
*::before,
*::after {
padding: 0;
margin: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
::-moz-focus-inner{
border: none;
}
.hide{
opacity: 0 !important;
}
:host{
display: grid;
--accent-color: #4d2588;
--text-color: 17, 17, 17;
--background-color: 255, 255, 255;
--danger-color: red;
--border-radius: 0.3rem;
--background: rgba(var(--text-color), 0.06);
--padding: initial;
--max-height: 8rem;
}
:host([variant="outlined"]) .textarea {
box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 0.4) inset;
background: rgba(var(--background-color), 1);
}
.textarea{
display: grid;
position: relative;
cursor: text;
min-width: 0;
text-align: left;
overflow: hidden auto;
grid-template-columns: 1fr;
align-items: stretch;
max-height: var(--max-height);
background: var(--background);
border-radius: var(--border-radius);
padding: var(--padding);
}
.textarea::after,
textarea{
padding: 0.7rem 1rem;
width: 100%;
min-width: 1em;
font: inherit;
color: inherit;
resize: none;
grid-area: 2/1;
justify-self: stretch;
background: none;
appearance: none;
border: none;
outline: none;
line-height: 1.5;
overflow: hidden;
}
.textarea::after{
content: attr(data-value) ' ';
visibility: hidden;
white-space: pre-wrap;
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
}
.readonly{
pointer-events: none;
}
.textarea:focus-within:not(.readonly){
box-shadow: 0 0 0 0.1rem var(--accent-color) inset;
}
.placeholder{
position: absolute;
margin: 0.7rem 1rem;
opacity: .7;
font-weight: inherit;
font-size: inherit;
line-height: 1.5;
pointer-events: none;
user-select: none;
}
:host([disabled]) .textarea{
cursor: not-allowed;
opacity: 0.6;
}
@media (any-hover: hover){
::-webkit-scrollbar{
width: 0.5rem;
height: 0.5rem;
}
::-webkit-scrollbar-thumb{
background: rgba(var(--text-color), 0.3);
border-radius: 1rem;
&:hover{
background: rgba(var(--text-color), 0.5);
}
}
}
</style>
<label class="textarea" part="textarea">
<span class="placeholder"></span>
<textarea rows="1"></textarea>
</label>
`;
customElements.define('sm-textarea',
class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(smTextarea.content.cloneNode(true))
this.textarea = this.shadowRoot.querySelector('textarea')
this.textareaBox = this.shadowRoot.querySelector('.textarea')
this.placeholder = this.shadowRoot.querySelector('.placeholder')
this.reflectedAttributes = ['disabled', 'required', 'readonly', 'rows', 'minlength', 'maxlength']
this.reset = this.reset.bind(this)
this.focusIn = this.focusIn.bind(this)
this.fireEvent = this.fireEvent.bind(this)
this.checkInput = this.checkInput.bind(this)
}
static get observedAttributes() {
return ['disabled', 'value', 'placeholder', 'required', 'readonly', 'rows', 'minlength', 'maxlength']
}
get value() {
return this.textarea.value
}
set value(val) {
this.setAttribute('value', val)
this.fireEvent()
}
get disabled() {
return this.hasAttribute('disabled')
}
set disabled(val) {
if (val) {
this.setAttribute('disabled', '')
} else {
this.removeAttribute('disabled')
}
}
get isValid() {
return this.textarea.checkValidity()
}
reset() {
this.setAttribute('value', '')
}
focusIn() {
this.textarea.focus()
}
fireEvent() {
let event = new Event('input', {
bubbles: true,
cancelable: true,
composed: true
});
this.dispatchEvent(event);
}
checkInput() {
if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder') === '')
return;
if (this.textarea.value !== '') {
this.placeholder.classList.add('hide')
} else {
this.placeholder.classList.remove('hide')
}
}
connectedCallback() {
this.textarea.addEventListener('input', e => {
this.textareaBox.dataset.value = this.textarea.value
this.checkInput()
})
}
attributeChangedCallback(name, oldValue, newValue) {
if (this.reflectedAttributes.includes(name)) {
if (this.hasAttribute(name)) {
this.textarea.setAttribute(name, this.getAttribute(name) ? this.getAttribute(name) : '')
}
else {
this.textContent.removeAttribute(name)
}
}
else if (name === 'placeholder') {
this.placeholder.textContent = this.getAttribute('placeholder')
}
else if (name === 'value') {
this.textarea.value = newValue;
this.textareaBox.dataset.value = newValue
this.checkInput()
}
}
})

View File

@ -132,7 +132,8 @@ a:any-link:focus-visible {
outline: rgba(var(--text-color), 1) 0.1rem solid;
}
sm-input {
sm-input,
sm-textarea {
font-size: 0.9rem;
--border-radius: 0.3rem;
}
@ -346,12 +347,8 @@ ul {
.empty-state {
display: grid;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
text-align: center;
width: 100%;
padding: 1.5rem;
padding: 1.5rem 1rem;
}
.observe-empty-state:empty {
@ -581,6 +578,11 @@ menu-option {
}
.article-link {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
position: relative;
padding: 1rem;
color: rgba(var(--text-color), 0.8);
font-weight: 500;
@ -590,15 +592,17 @@ menu-option {
text-transform: uppercase;
}
.default-article {
margin-left: auto;
.default-article::before {
-ms-flex-item-align: start;
align-self: flex-start;
content: "Actively written";
margin-bottom: 0.3rem;
font-size: 0.7rem;
background-color: #00e67650;
background-color: var(--accent-color);
padding: 0.2rem 0.4rem;
border-radius: 0.3rem;
font-weight: 700;
letter-spacing: 0.05em;
color: rgba(var(--text-color), 0.8);
border-radius: 0.2rem;
font-weight: 500;
color: var(--foreground-color);
}
#edit_sections_popup {
@ -614,21 +618,30 @@ menu-option {
.section-card {
font-weight: 500;
font-size: 0.9rem;
}
.section-card:not(.section-card--new) {
padding: 0.8rem 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.section-card--new input {
border: none;
font-size: inherit;
background: inherit;
color: inherit;
font-weight: inherit;
width: 100%;
padding: 0.8rem 0;
padding: 0.8rem 0.2rem;
}
.section-card--new input:focus {
outline: var(--accent-color) solid;
}
.section-card .remove {
padding: 0.3rem;
}
#insert_section_button {
-ms-flex-item-align: start;
@ -776,7 +789,7 @@ menu-option {
background-color: var(--accent-color);
}
.active .icon {
fill: white;
fill: var(--foreground-color);
}
.quote-template {
@ -864,8 +877,9 @@ menu-option {
background-color: #00e67650;
}
.entry__changes .removed {
-webkit-text-decoration-color: red;
text-decoration-color: red;
color: var(--danger-color);
-webkit-text-decoration-color: var(--danger-color);
text-decoration-color: var(--danger-color);
}
@media screen and (max-width: 40rem) and (any-hover: none) {

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -113,7 +113,8 @@ a:any-link:focus-visible {
outline: rgba(var(--text-color), 1) 0.1rem solid;
}
sm-input {
sm-input,
sm-textarea {
font-size: 0.9rem;
--border-radius: 0.3rem;
}
@ -309,10 +310,8 @@ ul {
}
.empty-state {
display: grid;
justify-content: center;
text-align: center;
width: 100%;
padding: 1.5rem;
padding: 1.5rem 1rem;
}
.observe-empty-state:empty {
@ -488,7 +487,6 @@ menu-option {
#main_page {
grid-template-columns: minmax(0, 1fr);
}
#current_article_title {
font-weight: 700;
}
@ -512,6 +510,8 @@ menu-option {
}
.article-link {
flex-direction: column;
position: relative;
padding: 1rem;
color: rgba(var(--text-color), 0.8);
font-weight: 500;
@ -521,14 +521,17 @@ menu-option {
}
}
.default-article {
margin-left: auto;
font-size: 0.7rem;
background-color: #00e67650;
padding: 0.2rem 0.4rem;
border-radius: 0.3rem;
font-weight: 700;
letter-spacing: 0.05em;
color: rgba(var(--text-color), 0.8);
&::before {
align-self: flex-start;
content: "Actively written";
margin-bottom: 0.3rem;
font-size: 0.7rem;
background-color: var(--accent-color);
padding: 0.2rem 0.4rem;
border-radius: 0.2rem;
font-weight: 500;
color: var(--foreground-color);
}
}
#edit_sections_popup {
@ -542,8 +545,10 @@ menu-option {
}
.section-card {
font-weight: 500;
font-size: 0.9rem;
&:not(.section-card--new) {
padding: 0.8rem 0;
user-select: none;
}
&--new {
input {
@ -551,13 +556,17 @@ menu-option {
font-size: inherit;
background: inherit;
color: inherit;
font-weight: inherit;
width: 100%;
padding: 0.8rem 0;
padding: 0.8rem 0.2rem;
&:focus {
outline: var(--accent-color) solid;
}
}
}
.remove {
padding: 0.3rem;
}
}
#insert_section_button {
align-self: flex-start;
@ -676,7 +685,7 @@ menu-option {
.active {
background-color: var(--accent-color);
.icon {
fill: white;
fill: var(--foreground-color);
}
}
@ -758,7 +767,8 @@ menu-option {
background-color: #00e67650;
}
.removed {
text-decoration-color: red;
color: var(--danger-color);
text-decoration-color: var(--danger-color);
}
}

View File

@ -101,6 +101,20 @@
</svg>
Edit sections
</menu-option>
<menu-option onclick="setDefaultArticle()">
<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>
<path d="M0,0h24v24H0V0z" fill="none" />
</g>
<g>
<path
d="M14,2H6C4.9,2,4.01,2.9,4.01,4L4,20c0,1.1,0.89,2,1.99,2H18c1.1,0,2-0.9,2-2V8L14,2z M18,20H6V4h7v5h5V20z M8.82,13.05 L7.4,14.46L10.94,18l5.66-5.66l-1.41-1.41l-4.24,4.24L8.82,13.05z" />
</g>
</svg>
Make default article
</menu-option>
</sm-menu>
</div>
<theme-toggle></theme-toggle>
@ -205,8 +219,8 @@
<h3>Create article</h3>
</header>
<sm-form>
<sm-input placeholder="Article title" required autofocus></sm-input>
<sm-checkbox checked>
<sm-input id="get_article_title" placeholder="Article title" required autofocus></sm-input>
<sm-checkbox id="set_default_checkbox" checked>
<div class="grid button__icon--right gap-0-5">
Set as default
<p style="font-size: 0.8rem;">
@ -214,15 +228,29 @@
</p>
</div>
</sm-checkbox>
<!-- <sm-switch>
<div slot="left">
Make private
<div class="grid gap-1">
<div class="grid gap-0-5">
<h4>Define sections (optional)</h4>
<p>Create and name sections by writing title of each section separated by a comma (,)</p>
</div>
</sm-switch> -->
<sm-button variant="primary">Create</sm-button>
<sm-textarea id="get_section_titles" rows="4" placeholder="section title 1, section title 2, ...">
</sm-textarea>
</div>
<!-- <div class="grid gap-1">
<div class="grid gap-0-5">
<sm-switch>
<h4 slot="left">
Make private
</h4>
</sm-switch>
<p>Define the FLO IDs separated by a comma (,), which are allowed to contribute.</p>
</div>
<sm-textarea id="get_section_titles" rows="4"></sm-textarea>
</div> -->
<sm-button variant="primary" onclick="cc.createNewArticle()">Create</sm-button>
</sm-form>
</sm-popup>
<sm-popup id="edit_sections_popup" open>
<sm-popup id="edit_sections_popup">
<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"
@ -239,26 +267,29 @@
<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 />
<div class="flex space-between align-center">
<sm-button id="insert_section_button" onclick="insertEmptySection()">
<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>
<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" />
<rect fill="none" height="24" width="24" />
</g>
</g>
</svg>
Add new section
</button>
<g>
<g />
<g>
<path
d="M17,19.22H5V7h7V5H5C3.9,5,3,5.9,3,7v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-7h-2V19.22z" />
<path d="M19,2h-2v3h-3c0.01,0.01,0,2,0,2h3v2.99c0.01,0.01,2,0,2,0V7h3V5h-3V2z" />
<rect height="2" width="8" x="7" y="9" />
<polygon points="7,12 7,14 15,14 15,12 12,12" />
<rect height="2" width="8" x="7" y="15" />
</g>
</g>
</svg>
Insert section
</sm-button>
<sm-button variant="primary" onclick="saveSectionEdit()">Save</sm-button>
</div>
</sm-popup>
<template id="section_template">
<text-field class="heading"></text-field>
@ -311,13 +342,20 @@
<p><br></p>
</template>
<template id="history_entry_template">
<li class="history-entry grid gap-0-5">
<li class="history-entry grid gap-1">
<div class="flex align-center space-between">
<time class="entry__time"></time>
<span class="entry__score"></span>
</div>
<div class="grid">
<div class="label">Author</div>
<div class="label flex align-center">
<svg class="icon" style="margin-right: 0.2rem;" width="24" height="24" viewBox="0 0 24 24"
fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M14.6243 7.99154C13.9395 8.14279 13.4165 8.69672 13.3047 9.38904C13.2248 9.88365 13.3664 10.3773 13.6735 10.7498L9.29125 15.0941C8.93718 15.4451 8.48852 15.6853 8.00011 15.7854L6.12418 16.1699L9.19811 13.1381C9.53438 12.8064 9.53812 12.265 9.20646 11.9287C8.8748 11.5924 8.33333 11.5887 7.99706 11.9203L4.97835 14.8977L5.47976 12.451C5.50451 12.3303 5.55656 12.2168 5.63191 12.1192L6.49083 11.0073C9.58386 7.00317 14.32 4.72705 19.2553 4.71054L16.3569 7.60884L14.6243 7.99154ZM2.79008 17.0559L3.8042 12.1076C3.88132 11.7313 4.0435 11.3776 4.27833 11.0736L5.13724 9.96172C8.89964 5.09105 14.861 2.53305 20.8954 3.07051C21.5971 3.133 22.2997 3.23735 23 3.38478L17.2133 9.1713L14.9932 9.66167L15.4238 9.91837C15.9039 10.2046 15.9849 10.8668 15.588 11.2603L10.4954 16.3088C9.90529 16.8938 9.15752 17.2941 8.34351 17.461L3.88965 18.3738L2.45572 19.788C2.11945 20.1197 1.57798 20.116 1.24632 19.7797C0.91466 19.4434 0.918397 18.9019 1.25467 18.5703L2.79008 17.0559Z" />
</svg>
Author
</div>
<span class="entry__author breakable"></span>
</div>
<div class="entry__changes"></div>
@ -412,29 +450,12 @@
}, delay);
}
class Stack {
constructor() {
this.items = [];
}
push(element) {
this.items.push(element);
}
pop() {
if (this.items.length == 0)
return "Underflow";
return this.items.pop();
}
peek() {
return this.items[this.items.length - 1];
}
}
let popupStack = new Stack()
let zIndex = 10
// function required for popups or modals to appear
function showPopup(popupId, pinned) {
zIndex++
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
popupStack = getRef(popupId).show({ pinned, popupStack })
getRef(popupId).show({ pinned })
return getRef(popupId);
}
@ -445,6 +466,29 @@
popupStack.peek().popup.hide()
}
document.addEventListener('popupopened', e => {
switch (e.target.id) {
case 'edit_sections_popup':
renderSectionList()
break;
case 'article_list_popup':
renderArticleList()
break;
}
})
document.addEventListener('popupclosed', e => {
zIndex--
switch (e.target.id) {
case 'edit_sections_popup':
getRef('section_list_container').innerHTML = ''
break;
case 'article_list_popup':
getRef('article_list').innerHTML = ``
break;
}
})
// displays a popup for asking permission. Use this instead of JS confirm
const getConfirmation = (title, options = {}) => {
return new Promise(resolve => {
@ -734,35 +778,62 @@
</script>
<script id="cc">
const cc = {
createNewArticle(title) {
const uid = floCrypto.randString(16, true)
floGlobals.appObjects.cc['defaultArticle'] = uid
floGlobals.appObjects.cc['articleList'][uid] = {
title,
timestamp: Date.now()
}
floCloudAPI.updateObjectData('cc')
.then((res) => {
console.log('created article entry', res)
})
.catch(err => console.error(err))
floGlobals.appObjects[uid] = {
public: true,
editors: [],
sections: [{
id: floCrypto.randString(16, true),
title: 'Introduction',
}, {
id: floCrypto.randString(16, true),
title: 'core',
createNewArticle() {
if (isSubAdmin) {
const title = getRef('get_article_title').value.trim()
const setDefault = getRef('set_default_checkbox').checked
const sectionTitles = getRef('get_section_titles').value.trim()
let sections = []
if (sectionTitles !== '') {
sections = sectionTitles.split(',').map(title => {
return {
id: floCrypto.randString(16, true),
title: title.trim(),
}
})
}
]
const uid = floCrypto.randString(16, true)
if (setDefault)
floGlobals.appObjects.cc['defaultArticle'] = uid
floGlobals.appObjects.cc['articleList'][uid] = {
title,
timestamp: Date.now()
}
floGlobals.appObjects[uid] = {
public: true,
editors: [],
sections
}
Promise.all([
floCloudAPI.updateObjectData('cc'),
floCloudAPI.resetObjectData(uid)
])
.then((res) => {
hidePopup()
notify('created article', 'success')
window.location.hash = `#/home?articleID=${uid}`
})
.catch(err => console.error(err))
} else {
notify('This action requires sub-admin privileges', 'error')
}
floCloudAPI.resetObjectData(uid)
.then((res) => {
console.log('created article', res)
})
.catch(err => console.error(err))
}
}
function setDefaultArticle() {
if (isSubAdmin) {
getConfirmation('Set as default article?').then(res => {
if (res) {
floGlobals.appObjects.cc['defaultArticle'] = currentArticle.id
floCloudAPI.updateObjectData('cc')
.then((res) => {
notify('Set current article as default', 'success')
})
.catch(err => console.error(err))
}
})
} else {
notify('This action requires sub-admin privileges', 'error')
}
}
@ -837,7 +908,7 @@
currentArticle.uniqueEntries[entry.origin] = { iterations: [{ ...entry, timestamp }] }
parentSection.firstElementChild.after(render.contentCard(entry.origin))
} else {
currentArticle.uniqueEntries[entry.origin].iterations.push({ ...entry, timestamp })
currentArticle.uniqueEntries[entry.origin].iterations.push({ ...entry, timestamp, editor: myFloID })
}
})
} else {
@ -930,7 +1001,6 @@
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))
@ -938,8 +1008,6 @@
}
if (!isSubAdmin) {
getRef('current_article_title').setAttribute('disabled', '')
} else {
renderSectionList()
}
getRef('current_article_title').value = title
getRef('article_wrapper').innerHTML = ''
@ -953,22 +1021,30 @@
className: 'article-link flex interact'
})
if (isDefaultArticle) {
link.append(createElement('span', {
className: 'default-article',
textContent: 'Actively written'
}))
link.classList.add('default-article')
}
return link
},
contentCard(id, version = 0) {
const clone = getRef('content_card_template').content.cloneNode(true).firstElementChild;
clone.dataset.uid = id
if (!floGlobals.subAdmins.includes(myFloID)) {
if (!isSubAdmin) {
clone.querySelector('.content__area').setAttribute('contentEditable', true)
}
const { data, contributors } = getIterationDetails(id)
console.log(contributors)
clone.querySelector('.content__area').innerHTML = DOMPurify.sanitize(data)
clone.querySelector('.content__editor').textContent = `by ${contributors}`;
if (contributors.size === 1) {
const [contributor] = contributors
clone.querySelector('.content__editor').textContent = `by ${contributor}`;
} else {
clone.querySelector('.content__editor').innerHTML = `
<svg class="icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6243 7.99154C13.9395 8.14279 13.4165 8.69672 13.3047 9.38904C13.2248 9.88365 13.3664 10.3773 13.6735 10.7498L9.29125 15.0941C8.93718 15.4451 8.48852 15.6853 8.00011 15.7854L6.12418 16.1699L9.19811 13.1381C9.53438 12.8064 9.53812 12.265 9.20646 11.9287C8.8748 11.5924 8.33333 11.5887 7.99706 11.9203L4.97835 14.8977L5.47976 12.451C5.50451 12.3303 5.55656 12.2168 5.63191 12.1192L6.49083 11.0073C9.58386 7.00317 14.32 4.72705 19.2553 4.71054L16.3569 7.60884L14.6243 7.99154ZM2.79008 17.0559L3.8042 12.1076C3.88132 11.7313 4.0435 11.3776 4.27833 11.0736L5.13724 9.96172C8.89964 5.09105 14.861 2.53305 20.8954 3.07051C21.5971 3.133 22.2997 3.23735 23 3.38478L17.2133 9.1713L14.9932 9.66167L15.4238 9.91837C15.9039 10.2046 15.9849 10.8668 15.588 11.2603L10.4954 16.3088C9.90529 16.8938 9.15752 17.2941 8.34351 17.461L3.88965 18.3738L2.45572 19.788C2.11945 20.1197 1.57798 20.116 1.24632 19.7797C0.91466 19.4434 0.918397 18.9019 1.25467 18.5703L2.79008 17.0559Z"/>
</svg>
`
}
// clone.querySelector('.content__score').textContent = score;
return clone
},
@ -1008,7 +1084,7 @@
const frag = document.createDocumentFragment()
section.children[0].dataset.index = index
section.children[1].dataset.sectionId = sectionID
if (floGlobals.subAdmins.includes(myFloID)) {
if (isSubAdmin) {
section.querySelector('.content-card--empty').remove()
} else {
section.querySelector('.heading').setAttribute('disabled', '')
@ -1066,6 +1142,7 @@
}
}
function renderSectionList() {
if (!isSubAdmin) return
const frag = document.createDocumentFragment()
floGlobals.appObjects[currentArticle.id].sections.forEach(section => {
frag.append(render.sectionCard(section))
@ -1088,7 +1165,49 @@
`
})
getRef('section_list_container').append(emptySection)
emptySection.querySelector('input').focus()
}
getRef('section_list_container').addEventListener('click', e => {
if (e.target.closest('.remove')) {
e.target.closest('.section-card').remove()
}
})
function saveSectionEdit() {
const newSections = []
getRef('section_list_container').querySelectorAll('.section-card--new').forEach(section => {
const title = section.querySelector('input').value.trim()
if (title !== '')
newSections.push({
id: floCrypto.randString(16, true),
title
})
})
if (newSections.length) {
newSections.forEach(elem => {
floGlobals.appObjects[currentArticle.id].sections.push(elem)
})
floCloudAPI.updateObjectData(currentArticle.id)
.then((res) => {
const frag = document.createDocumentFragment()
const currentSectionCount = getRef('section_list_container').querySelectorAll('.section-card:not(.section-card--new)').length
let index = currentSectionCount
newSections.forEach(elem => {
const { title, id } = elem
currentArticle.sections[id] = { title, uniqueEntries: [] }
frag.append(render.section(id, currentArticle.sections[id], index))
index += 1
})
getRef('article_wrapper').append(frag)
notify('Sections updated', 'success')
hidePopup()
})
.catch(err => {
notify(err, 'error')
})
}
}
function getArticleList() {
const articleList = floGlobals.appObjects.cc.articleList
@ -1114,7 +1233,7 @@
const searchKey = e.target.value.trim()
const options = {
keys: ['title'],
threshold: 0
threshold: 0.3
}
const fuse = new Fuse(getArticleList(), options)
renderArticleList(searchKey === '' ? undefined : fuse.search(searchKey).map(v => v.item))
@ -1234,6 +1353,7 @@
},
200)
const replaceBetween = (origin, startIndex, endIndex, insertion) =>
`${origin.substring(0, startIndex)}${insertion}${origin.substring(endIndex)}`;
const make = {
@ -1357,6 +1477,7 @@
}
</script>
<script id="onLoadStartUp">
let isSubAdmin = false
function onLoadStartUp() {
//floDapps.addStartUpFunction('Sample', Promised Function)
@ -1364,11 +1485,11 @@
//floDapps.setCustomPrivKeyInput( () => { FUNCTION BODY *must resolve private key* } )
floDapps.launchStartUp().then(async result => {
isSubAdmin = floGlobals.subAdmins.includes(myFloID)
await Promise.all([
floCloudAPI.requestObjectData('cc'),
])
showPage(window.location.hash)
renderArticleList()
console.log(result)
// alert(`Welcome FLO_ID: ${ myFloID }`)