Updated article title and section changing UI and logic

This commit is contained in:
sairaj mote 2022-01-28 01:48:07 +05:30
parent 250b0b8d41
commit 7afbf8257f
5 changed files with 125 additions and 373 deletions

View File

@ -3553,238 +3553,6 @@ customElements.define('sm-switch', class extends HTMLElement {
})
const textField = document.createElement('template')
textField.innerHTML = `
<style>
*{
padding: 0;
margin: 0;
box-sizing: border-box;
}
:host{
--accent-color: #4d2588;
--text-color: 17, 17, 17;
--background-color: 255, 255, 255;
}
.text-field{
display: flex;
align-items: center;
}
.text{
transition: background-color 0.3s;
border-bottom: 0.15rem solid transparent;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
word-break: break-word;
-moz-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
.text:focus{
outline: none;
border-bottom: 0.15rem solid var(--accent-color);
}
.text:focus-visible{
outline: none;
background: solid rgba(var(--text-color), 0.06);
}
.editable{
border-bottom: 0.15rem solid rgba(var(--text-color), 0.6);
}
.edit-button{
display: grid;
position: relative;
margin-left: 0.5rem;
background-color: transparent;
border: none;
}
:host([disabled]) .edit-button{
display: none;
}
.icon{
grid-area: 1/-1;
cursor: pointer;
height: 1.2rem;
width: 1.2rem;
fill: rgba(var(--text-color), 1);
}
.hide{
visibility: hidden;
}
</style>
<div class="text-field">
<div class="text" part="text"></div>
<button class="edit-button">
<svg class="icon" 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 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"/></svg>
<svg class="icon hide" title="Save" 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="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>
</button>
</div>
`
customElements.define('text-field', class extends HTMLElement {
constructor() {
super()
this.attachShadow({
mode: 'open'
}).append(textField.content.cloneNode(true))
this.textField = this.shadowRoot.querySelector('.text-field')
this.textContainer = this.textField.children[0]
this.iconsContainer = this.textField.children[1]
this.editButton = this.textField.querySelector('.edit-button')
this.isTextEditable = false
this.isDisabled = false
this.fireEvent = this.fireEvent.bind(this)
this.setEditable = this.setEditable.bind(this)
this.setNonEditable = this.setNonEditable.bind(this)
this.toggleEditable = this.toggleEditable.bind(this)
this.revert = this.revert.bind(this)
}
static get observedAttributes() {
return ['disabled', 'value']
}
get value() {
return this.text
}
set value(val) {
this.setAttribute('value', val)
}
set disabled(val) {
this.isDisabled = val
if (this.isDisabled)
this.setAttribute('disabled', '')
else
this.removeAttribute('disabled')
}
fireEvent(value) {
let event = new CustomEvent('change', {
bubbles: true,
cancelable: true,
composed: true,
detail: {
value
}
});
this.dispatchEvent(event);
}
setEditable() {
if (this.isTextEditable) return
this.textContainer.contentEditable = true
this.textContainer.classList.add('editable')
this.textContainer.focus()
document.execCommand('selectAll', false, null);
this.editButton.children[0].animate(this.rotateOut, this.animOptions).onfinish = () => {
this.editButton.children[0].classList.add('hide')
}
setTimeout(() => {
this.editButton.children[1].classList.remove('hide')
this.editButton.children[1].animate(this.rotateIn, this.animOptions)
}, 100);
this.isTextEditable = true
}
setNonEditable() {
if (!this.isTextEditable) return
this.textContainer.contentEditable = false
this.textContainer.classList.remove('editable')
const newValue = this.textContainer.textContent.trim()
if (this.text !== newValue && newValue !== '') {
this.setAttribute('value', this.textContainer.textContent)
this.text = this.textContainer.textContent.trim()
this.fireEvent(this.text)
} else {
this.value = this.text
}
this.editButton.children[1].animate(this.rotateOut, this.animOptions).onfinish = () => {
this.editButton.children[1].classList.add('hide')
}
setTimeout(() => {
this.editButton.children[0].classList.remove('hide')
this.editButton.children[0].animate(this.rotateIn, this.animOptions)
}, 100);
this.isTextEditable = false
}
toggleEditable() {
if (this.isTextEditable)
this.setNonEditable()
else
this.setEditable()
}
revert() {
if (this.textContainer.isContentEditable) {
this.value = this.text
this.setNonEditable()
}
}
connectedCallback() {
this.text
if (this.hasAttribute('value')) {
this.text = this.getAttribute('value')
this.textContainer.textContent = this.text
}
if (this.hasAttribute('disabled'))
this.isDisabled = true
else
this.isDisabled = false
this.rotateOut = [
{
transform: 'rotate(0)',
opacity: 1
},
{
transform: 'rotate(90deg)',
opacity: 0
},
]
this.rotateIn = [
{
transform: 'rotate(-90deg)',
opacity: 0
},
{
transform: 'rotate(0)',
opacity: 1
},
]
this.animOptions = {
duration: 300,
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
fill: 'forwards'
}
if (!this.isDisabled) {
this.iconsContainer.classList.remove('hide')
this.textContainer.addEventListener('dblclick', this.setEditable)
this.editButton.addEventListener('click', this.toggleEditable)
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'disabled') {
if (this.hasAttribute('disabled')) {
this.textContainer.removeEventListener('dblclick', this.setEditable)
this.editButton.removeEventListener('click', this.toggleEditable)
this.revert()
}
else {
this.textContainer.addEventListener('dblclick', this.setEditable)
this.editButton.addEventListener('click', this.toggleEditable)
}
} else if (name === 'value') {
this.text = newValue
this.textContainer.textContent = newValue
}
}
disconnectedCallback() {
this.textContainer.removeEventListener('dblclick', this.setEditable)
this.editButton.removeEventListener('click', this.toggleEditable)
}
})
const smMenu = document.createElement('template')
smMenu.innerHTML = `
<style>

View File

@ -620,6 +620,7 @@ sm-copy {
height: 100%;
overflow-y: hidden;
grid-template-columns: minmax(0, 1fr);
align-content: flex-start;
}
#current_article_title {
@ -671,33 +672,24 @@ sm-copy {
}
#section_list_container {
background-color: rgba(var(--text-color), 0.06);
margin: 1rem 0;
padding: 0 0.8rem;
border-radius: 0.5rem;
gap: 0.5rem;
}
.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 {
.section-card input {
background-color: rgba(var(--text-color), 0.06);
padding: 0.8rem;
border-radius: 0.3rem;
border: none;
font-size: inherit;
background: inherit;
color: inherit;
font-weight: inherit;
width: 100%;
padding: 0.8rem 0.2rem;
}
.section-card--new input:focus {
.section-card input:focus {
outline: var(--accent-color) solid;
}
.section-card .remove {
@ -1071,12 +1063,6 @@ sm-copy {
display: none;
}
}
@supports (-webkit-text-stroke: 1px black) {
#landing h1 {
-webkit-text-stroke: 1px rgba(var(--text-color), 1);
-webkit-text-fill-color: rgba(var(--background-color), 1);
}
}
@media screen and (min-width: 40rem) {
sm-popup {
--width: 24rem;
@ -1115,10 +1101,6 @@ sm-copy {
display: none;
}
#landing h1 {
-webkit-text-stroke-width: 0.1rem;
}
#main_header {
padding: 1rem 1.5rem;
grid-template-columns: auto 1fr auto auto;

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -588,6 +588,7 @@ sm-copy {
height: 100%;
overflow-y: hidden;
grid-template-columns: minmax(0, 1fr);
align-content: flex-start;
}
#current_article_title {
font-weight: 700;
@ -638,30 +639,22 @@ sm-copy {
--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;
gap: 0.5rem;
}
.section-card {
font-weight: 500;
font-size: 0.9rem;
&:not(.section-card--new) {
padding: 0.8rem 0;
user-select: none;
}
&--new {
input {
border: none;
font-size: inherit;
background: inherit;
color: inherit;
font-weight: inherit;
width: 100%;
padding: 0.8rem 0.2rem;
&:focus {
outline: var(--accent-color) solid;
}
input {
background-color: rgba(var(--text-color), 0.06);
padding: 0.8rem;
border-radius: 0.3rem;
border: none;
font-size: inherit;
color: inherit;
font-weight: inherit;
width: 100%;
&:focus {
outline: var(--accent-color) solid;
}
}
.remove {
@ -1011,12 +1004,6 @@ sm-copy {
display: none;
}
}
@supports (-webkit-text-stroke: 1px black) {
#landing h1 {
-webkit-text-stroke: 1px rgba(var(--text-color), 1);
-webkit-text-fill-color: rgba(var(--background-color), 1);
}
}
@media screen and (min-width: 40rem) {
sm-popup {
--width: 24rem;
@ -1049,9 +1036,6 @@ sm-copy {
.hide-on-desktop {
display: none;
}
#landing h1 {
-webkit-text-stroke-width: 0.1rem;
}
#main_header {
padding: 1rem 1.5rem;
grid-template-columns: auto 1fr auto auto;

View File

@ -167,7 +167,7 @@
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>
<h4 id="current_article_title"></h4>
<button id="article_outline_button" class="icon-only" title="View article outline"
onclick="toggleOutlinePanel()">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px"
@ -204,7 +204,7 @@
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
Edit title & sections
</menu-option>
<menu-option onclick="setDefaultArticle()">
<svg class="icon button__icon--left" xmlns="http://www.w3.org/2000/svg"
@ -473,36 +473,44 @@
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>
<h3>Edit title & 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>
<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>
<rect fill="none" height="24" width="24" />
</g>
<g>
<g />
<section class="grid gap-1-5">
<div class="grid gap-0-5">
<h5 class="label">Title</h5>
<sm-input id="edit_article_title"></sm-input>
</div>
<div class="grid gap-0-5">
<h5 class="label">Sections</h5>
<div id="section_list_container" class="observe-empty-state grid"></div>
<p class="empty-state">
There are no sections so far, you can add section with button below.
</p>
</div>
<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>
Insert section
</sm-button>
<sm-button variant="primary" onclick="saveSectionEdit()">Save</sm-button>
</div>
<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>
</section>
</sm-popup>
<sm-popup id="contributors_popup">
<header slot="header" class="popup__header">
@ -594,7 +602,7 @@
</template>
<template id="section_template">
<div class="heading flex align-center">
<text-field></text-field>
<h4 class="section-title"></h4>
</div>
<section class="article-section">
<div class="content-card content-card--empty">
@ -1343,7 +1351,7 @@
break
}
if (noOfContributors < 2 && !contributors.hasOwnProperty(myFloID)) {
contentCard.querySelector('.content__author').textContent = `2 Contributors`
contentCard.querySelector('.content__author').textContent = `${myFloID} and 1 more`
}
floGlobals.currentArticle.uniqueEntries[entry.origin].iterations.push(genDataVC)
}
@ -1462,14 +1470,6 @@
animateTo(getRef('article_name_wrapper'), slideInRight, animOptions)
}
}
} else if (e.target.closest('text-field')) {
const heading = e.target.closest('.heading');
floGlobals.appObjects[floGlobals.currentArticle.id].sections[heading.dataset.index].title = e.target.value.trim()
floCloudAPI.updateObjectData(floGlobals.currentArticle.id)
.then((res) => {
notify('Updated heading', 'success')
})
.catch(err => console.error(err))
}
})
@ -1724,14 +1724,6 @@
currentElement.remove()
}
})
getRef('current_article_title').addEventListener("change", e => {
floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title = e.target.value.trim()
floCloudAPI.updateObjectData('cc')
.then((res) => {
notify('Renamed article', 'success')
})
.catch(err => console.error(err))
})
getRef('article_wrapper').addEventListener("focusin", e => {
if (e.target.closest('.content__area')) {
const target = e.target.closest('.content__area')
@ -1872,24 +1864,10 @@
frag.append(render.section(sectionID, sections[sectionID], index))
index += 1
}
if (!floGlobals.isSubAdmin) {
getRef('current_article_title').setAttribute('disabled', '')
}
getRef('current_article_title').value = title
getRef('current_article_title').textContent = title
getRef('article_wrapper').innerHTML = ''
getRef('article_wrapper').append(frag)
getRef('article_outline').innerHTML = ''
floGlobals.appObjects[floGlobals.currentArticle.id].sections.forEach(section => {
frag.append(createElement('button', {
attributes: { 'data-section-id': section.id },
className: 'outline-button',
textContent: section.title,
}))
})
getRef('article_outline').append(frag)
render.sectionOutline()
},
articleLink(details, isDefaultArticle) {
const { uid, timestamp, title } = details
@ -1964,18 +1942,15 @@
}
return clone
},
section(sectionID, { title, uniqueEntries }, index) {
section(sectionID, { title, uniqueEntries }) {
const section = getRef('section_template').content.cloneNode(true)
const frag = document.createDocumentFragment()
section.children[0].dataset.index = index
section.children[0].dataset.sectionId = sectionID
section.children[1].dataset.sectionId = sectionID
if (floGlobals.isSubAdmin) {
section.querySelector('.content-card--empty').remove()
} else {
section.querySelector('text-field').setAttribute('disabled', '')
}
section.querySelector('text-field').setAttribute('value', title)
section.querySelector('.section-title').textContent = title
floGlobals.currentArticle.sections[sectionID].uniqueEntries.slice(0, maxCardsPerSection).forEach(entry => {
const contentCard = render.contentCard(entry)
if (contentCard)
@ -1994,14 +1969,27 @@
return section
},
sectionCard(details) {
const { title } = details
const { title, id } = details
return createElement('div', {
className: 'section-card flex align-center',
attributes: { 'data-section-id': id },
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}
<input placeholder="Section title" value="${title}"/>
`
})
},
sectionOutline() {
const frag = document.createDocumentFragment()
getRef('article_outline').innerHTML = ''
floGlobals.appObjects[floGlobals.currentArticle.id].sections.forEach(section => {
frag.append(createElement('button', {
attributes: { 'data-section-id': section.id },
className: 'outline-button',
textContent: section.title,
}))
})
getRef('article_outline').append(frag)
}
}
@ -2174,6 +2162,7 @@
getRef('sort_content_list').addEventListener('change', sortSectionEntries)
function renderSectionList() {
if (!floGlobals.isSubAdmin) return
getRef('edit_article_title').value = floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title
const frag = document.createDocumentFragment()
floGlobals.appObjects[floGlobals.currentArticle.id].sections.forEach(section => {
frag.append(render.sectionCard(section))
@ -2183,7 +2172,7 @@
}
function insertEmptySection() {
const emptySection = createElement('div', {
className: 'section-card section-card--new flex align-center',
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>
<input placeholder="New section title"/>
@ -2206,29 +2195,58 @@
function saveSectionEdit() {
if (floGlobals.isSubAdmin) {
const newSections = []
getRef('section_list_container').querySelectorAll('.section-card--new').forEach(section => {
const newArticleTitle = getRef('edit_article_title').value.trim()
if (newArticleTitle !== '') {
if (floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title !== newArticleTitle) {
floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title = newArticleTitle
floCloudAPI.updateObjectData('cc')
.then((res) => {
getRef('current_article_title').textContent = newArticleTitle
notify('Renamed article', 'success')
}).catch(err => {
notify(err, 'error')
})
}
} else {
getRef('edit_article_title').value = floGlobals.appObjects.cc.articleList[floGlobals.currentArticle.id].title
}
const changedSections = [];
[...getRef('section_list_container').children].forEach(section => {
const title = section.querySelector('input').value.trim()
const sectionID = section.dataset.sectionId
if (title !== '')
newSections.push({
id: floCrypto.randString(16, true),
changedSections.push({
id: sectionID || floCrypto.randString(16, true),
title
})
})
if (newSections.length) {
newSections.forEach(elem => {
floGlobals.appObjects[floGlobals.currentArticle.id].sections.push(elem)
})
const didAddSection = changedSections.length > floGlobals.appObjects[floGlobals.currentArticle.id].sections.length
let didTitlesChange = false
if (!didAddSection) {
didTitlesChange = floGlobals.appObjects[floGlobals.currentArticle.id].sections.some(({ title }, index) => title !== changedSections[index].title)
}
if (didAddSection || didTitlesChange) {
floGlobals.appObjects[floGlobals.currentArticle.id].sections = changedSections
floCloudAPI.updateObjectData(floGlobals.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 => {
render.sectionOutline()
const frag = document.createDocumentFragment();
const currentSections = {}
getRef('article_wrapper').querySelectorAll('.heading').forEach(heading => {
currentSections[heading.dataset.sectionId] = {
title: heading.textContent,
ref: heading
}
})
changedSections.forEach(elem => {
const { title, id } = elem
floGlobals.currentArticle.sections[id] = { title, uniqueEntries: [] }
frag.append(render.section(id, floGlobals.currentArticle.sections[id], index))
index += 1
if (currentSections.hasOwnProperty(id)) {
currentSections[id].ref.textContent = title
} else {
floGlobals.currentArticle.sections[id] = { title, uniqueEntries: [] }
frag.append(render.section(id, floGlobals.currentArticle.sections[id]))
}
})
getRef('article_wrapper').append(frag)
notify('Sections updated', 'success')