Feature update and bug fixes
- Added article title and heading renaming in-place for sub-admins - fixed bug for version history - version history UX improvements
This commit is contained in:
parent
fed6e720f6
commit
e155bfc341
230
components.js
230
components.js
@ -3427,4 +3427,234 @@ 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{
|
||||||
|
padding: 0.6rem 0;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
.icon-container{
|
||||||
|
display: grid;
|
||||||
|
position: relative;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
:host([disabled]) .icon-container{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.icon{
|
||||||
|
grid-area: 1/-1;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 1.2rem;
|
||||||
|
width: 1.2rem;
|
||||||
|
fill: rgba(var(--text-color), 1);
|
||||||
|
}
|
||||||
|
.hide{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="text-field">
|
||||||
|
<div class="text" part="text"></div>
|
||||||
|
<div tabindex="0" class="icon-container">
|
||||||
|
<svg class="edit-button 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="save-button 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>
|
||||||
|
</div>
|
||||||
|
</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.saveButton = this.textField.querySelector('.save-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.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.animate(this.rotateOut, this.animOptions).onfinish = () => {
|
||||||
|
this.editButton.classList.add('hide')
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saveButton.classList.remove('hide')
|
||||||
|
this.saveButton.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.saveButton.animate(this.rotateOut, this.animOptions).onfinish = () => {
|
||||||
|
this.saveButton.classList.add('hide')
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.editButton.classList.remove('hide')
|
||||||
|
this.editButton.animate(this.rotateIn, this.animOptions)
|
||||||
|
}, 100);
|
||||||
|
this.isTextEditable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
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.setEditable)
|
||||||
|
this.saveButton.addEventListener('click', this.setNonEditable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attributeChangedCallback(name, oldValue, newValue) {
|
||||||
|
if (name === 'disabled') {
|
||||||
|
if (this.hasAttribute('disabled')) {
|
||||||
|
this.textContainer.removeEventListener('dblclick', this.setEditable)
|
||||||
|
this.editButton.removeEventListener('click', this.setEditable)
|
||||||
|
this.saveButton.removeEventListener('click', this.setNonEditable)
|
||||||
|
this.revert()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.textContainer.addEventListener('dblclick', this.setEditable)
|
||||||
|
this.editButton.addEventListener('click', this.setEditable)
|
||||||
|
this.saveButton.addEventListener('click', this.setNonEditable)
|
||||||
|
}
|
||||||
|
} else if (name === 'value') {
|
||||||
|
this.text = newValue
|
||||||
|
this.textContainer.textContent = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.textContainer.removeEventListener('dblclick', this.setEditable)
|
||||||
|
this.editButton.removeEventListener('click', this.setEditable)
|
||||||
|
this.saveButton.removeEventListener('click', this.setNonEditable)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
38
css/main.css
38
css/main.css
@ -552,6 +552,31 @@ sm-checkbox {
|
|||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#current_article_title {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-link {
|
||||||
|
padding: 1rem;
|
||||||
|
color: rgba(var(--text-color), 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
}
|
||||||
|
.article-link::first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
#article_wrapper {
|
#article_wrapper {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -ms-flexbox;
|
display: -ms-flexbox;
|
||||||
@ -565,6 +590,7 @@ sm-checkbox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
|
font-weight: 700;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -ms-flexbox;
|
display: -ms-flexbox;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -592,16 +618,12 @@ sm-checkbox {
|
|||||||
.article-section:not(:last-of-type) {
|
.article-section:not(:last-of-type) {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
.article-section::-webkit-scrollbar {
|
||||||
.content-card-container {
|
display: none;
|
||||||
display: -webkit-box;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-card {
|
.content-card {
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: center;
|
||||||
width: min(46ch, 100%);
|
width: min(46ch, 100%);
|
||||||
-ms-flex-negative: 0;
|
-ms-flex-negative: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@ -778,7 +800,7 @@ sm-checkbox {
|
|||||||
}
|
}
|
||||||
.entry__changes .added > *,
|
.entry__changes .added > *,
|
||||||
.entry__changes .removed > * {
|
.entry__changes .removed > * {
|
||||||
background-color: inherit;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
.entry__changes .added {
|
.entry__changes .added {
|
||||||
background-color: #00e67650;
|
background-color: #00e67650;
|
||||||
|
|||||||
2
css/main.min.css
vendored
2
css/main.min.css
vendored
File diff suppressed because one or more lines are too long
@ -485,6 +485,30 @@ sm-checkbox {
|
|||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#current_article_title {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-link {
|
||||||
|
padding: 1rem;
|
||||||
|
color: rgba(var(--text-color), 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
&::first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
#article_wrapper {
|
#article_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -493,6 +517,7 @@ sm-checkbox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
|
font-weight: 700;
|
||||||
display: flex;
|
display: flex;
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
@ -513,14 +538,12 @@ sm-checkbox {
|
|||||||
&:not(:last-of-type) {
|
&:not(:last-of-type) {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.content-card-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-card {
|
.content-card {
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: center;
|
||||||
width: min(46ch, 100%);
|
width: min(46ch, 100%);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
@ -675,7 +698,7 @@ sm-checkbox {
|
|||||||
color: rgba(var(--text-color), 0.8);
|
color: rgba(var(--text-color), 0.8);
|
||||||
.added > *,
|
.added > *,
|
||||||
.removed > * {
|
.removed > * {
|
||||||
background-color: inherit;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
.added {
|
.added {
|
||||||
background-color: #00e67650;
|
background-color: #00e67650;
|
||||||
|
|||||||
292
index.html
292
index.html
@ -80,7 +80,7 @@
|
|||||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<h4 id="current_article_name"></h4>
|
<text-field id="current_article_title"></text-field>
|
||||||
<button class="button__icon--right" onclick="showPopup('article_list_popup')">
|
<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"
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
||||||
fill="#000000">
|
fill="#000000">
|
||||||
@ -189,6 +189,7 @@
|
|||||||
<sm-input placeholder="Search articles" type="search"></sm-input>
|
<sm-input placeholder="Search articles" type="search"></sm-input>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<div id="article_list"></div>
|
||||||
</sm-popup>
|
</sm-popup>
|
||||||
<sm-popup id="create_article_popup">
|
<sm-popup id="create_article_popup">
|
||||||
<header slot="header" class="popup__header">
|
<header slot="header" class="popup__header">
|
||||||
@ -221,14 +222,14 @@
|
|||||||
</sm-form>
|
</sm-form>
|
||||||
</sm-popup>
|
</sm-popup>
|
||||||
<template id="section_template">
|
<template id="section_template">
|
||||||
<h4 class="heading"></h4>
|
<text-field class="heading"></text-field>
|
||||||
<section class="article-section">
|
<section class="article-section">
|
||||||
<div class="content-card content-card--empty">
|
<div class="content-card content-card--empty">
|
||||||
<div class="content__area" data-type="origin" placeholder="Write something new or edit existing content"
|
<div class="content__area" data-type="origin" placeholder="Write something new or edit existing content"
|
||||||
contenteditable="true"></div>
|
contenteditable="true"></div>
|
||||||
<button class="submit-entry">Submit</button>
|
<button class="submit-entry hide-completely">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-card-container"></div>
|
<!-- <div class="content-card-container"></div> -->
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<template id="content_card_template">
|
<template id="content_card_template">
|
||||||
@ -251,11 +252,11 @@
|
|||||||
<path
|
<path
|
||||||
d="M12 7.13l.97 2.29.47 1.11 1.2.1 2.47.21-1.88 1.63-.91.79.27 1.18.56 2.41-2.12-1.28-1.03-.64-1.03.62-2.12 1.28.56-2.41.27-1.18-.91-.79-1.88-1.63 2.47-.21 1.2-.1.47-1.11.97-2.27M12 2L9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2z" />
|
d="M12 7.13l.97 2.29.47 1.11 1.2.1 2.47.21-1.88 1.63-.91.79.27 1.18.56 2.41-2.12-1.28-1.03-.64-1.03.62-2.12 1.28.56-2.41.27-1.18-.91-.79-1.88-1.63 2.47-.21 1.2-.1.47-1.11.97-2.27M12 2L9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="content__score">47</span>
|
<span class="content__score">0</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="content__editor"></div>
|
<div class="content__editor"></div>
|
||||||
<button class="submit-entry">Submit</button>
|
<button class="submit-entry hide-completely">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -533,51 +534,26 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (targetPage.includes('/')) {
|
if (targetPage.includes('/')) {
|
||||||
const pages = targetPage.split('/')
|
if (targetPage.includes('?')) {
|
||||||
pageId = pages[1]
|
const splitAddress = targetPage.split('?')
|
||||||
|
searchParams = splitAddress.pop()
|
||||||
|
const pages = splitAddress.pop().split('/')
|
||||||
|
pageId = pages[1]
|
||||||
|
subPageId = pages[2]
|
||||||
|
} else {
|
||||||
|
const pages = targetPage.split('/')
|
||||||
|
pageId = pages[1]
|
||||||
|
subPageId = pages[2]
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
pageId = targetPage
|
pageId = targetPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pagesData.lastPage !== pageId) {
|
if (pagesData.lastPage !== pageId) {
|
||||||
let target
|
document.querySelectorAll('.mobile-page').forEach(elem => elem.classList.add('hide-on-mobile'))
|
||||||
switch (pageId) {
|
pagesData.lastPage = pageId
|
||||||
case 'dashboard':
|
if (!pagesData.openedPages.includes(pageId)) {
|
||||||
target = 'dashboard'
|
pagesData.openedPages.push(pageId)
|
||||||
break;
|
|
||||||
case 'my_orders':
|
|
||||||
target = 'my_orders_section'
|
|
||||||
break;
|
|
||||||
case 'market':
|
|
||||||
target = 'market_orders_section'
|
|
||||||
break;
|
|
||||||
case 'wallet':
|
|
||||||
target = 'user_section'
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (target) {
|
|
||||||
document.querySelectorAll('.mobile-page').forEach(elem => elem.classList.add('hide-on-mobile'))
|
|
||||||
getRef(target).classList.remove('hide-on-mobile')
|
|
||||||
document.querySelectorAll('.bottom_nav__item').forEach(elem => elem.classList.remove('bottom_nav__item--active'))
|
|
||||||
document.querySelector(`.bottom_nav__item[href="#/${pageId}"]`).classList.add('bottom_nav__item--active')
|
|
||||||
getRef(target)?.animate([
|
|
||||||
{
|
|
||||||
transform: 'translateY(1rem)',
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
transform: 'none',
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
duration: 300,
|
|
||||||
easing: 'ease'
|
|
||||||
})
|
|
||||||
pagesData.lastPage = target
|
|
||||||
if (!pagesData.openedPages.includes(target)) {
|
|
||||||
pagesData.openedPages.push(target)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -732,26 +708,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkEntry = debounce(e => {
|
||||||
|
const contentCard = e.target.closest('.content-card')
|
||||||
|
const contentArea = contentCard.querySelector('.content__area')
|
||||||
|
const uid = contentCard.dataset.uid
|
||||||
|
const isUniqueEntry = contentArea.dataset.type === 'origin'
|
||||||
|
contentArea.querySelectorAll('[style=""]').forEach((el) => {
|
||||||
|
el.removeAttribute('style')
|
||||||
|
})
|
||||||
|
const clean = DOMPurify.sanitize(contentArea.innerHTML.split('\n').map(v => v.trim()).filter(v => v));
|
||||||
|
if (clean === '' || clean === '<p><br></p>') {
|
||||||
|
contentCard.querySelector('.submit-entry').classList.add('hide-completely')
|
||||||
|
} else {
|
||||||
|
const hash = Crypto.SHA256(clean)
|
||||||
|
let previousHash
|
||||||
|
if (!isUniqueEntry) {
|
||||||
|
({ hash: previousHash = '' } = getIterationDetails(uid))
|
||||||
|
} else {
|
||||||
|
previousHash = ''
|
||||||
|
}
|
||||||
|
if (previousHash !== hash)
|
||||||
|
contentCard.querySelector('.submit-entry').classList.remove('hide-completely')
|
||||||
|
else
|
||||||
|
contentCard.querySelector('.submit-entry').classList.add('hide-completely')
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 300)
|
||||||
|
getRef('article_wrapper').addEventListener('input', e => {
|
||||||
|
if (e.target.closest('.content__area')) {
|
||||||
|
checkEntry(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
getRef('article_wrapper').addEventListener('click', e => {
|
getRef('article_wrapper').addEventListener('click', e => {
|
||||||
if (e.target.closest('.submit-entry')) {
|
if (e.target.closest('.submit-entry')) {
|
||||||
const contentCard = e.target.closest('.content-card')
|
const contentCard = e.target.closest('.content-card')
|
||||||
const contentArea = contentCard.querySelector('.content__area')
|
const contentArea = contentCard.querySelector('.content__area')
|
||||||
const uid = contentCard.dataset.uid
|
const uid = contentCard.dataset.uid
|
||||||
const isUniqueEntry = contentArea.dataset.type === 'origin'
|
const isUniqueEntry = contentArea.dataset.type === 'origin'
|
||||||
|
const parentSection = contentCard.closest('.article-section')
|
||||||
|
const sectionID = parentSection.dataset.sectionId
|
||||||
contentArea.querySelectorAll('[style=""]').forEach((el) => {
|
contentArea.querySelectorAll('[style=""]').forEach((el) => {
|
||||||
el.removeAttribute('style')
|
el.removeAttribute('style')
|
||||||
})
|
})
|
||||||
const clean = DOMPurify.sanitize(contentArea.innerHTML.split('\n').map(v => v.trim()).filter(v => v));
|
const clean = DOMPurify.sanitize(contentArea.innerHTML.split('\n').map(v => v.trim()).filter(v => v));
|
||||||
// const clean = contentArea.innerText.trim();
|
|
||||||
|
|
||||||
if (clean === '') return
|
if (clean === '') return
|
||||||
const hash = Crypto.SHA256(clean)
|
const hash = Crypto.SHA256(clean)
|
||||||
let previousVersion, contributors
|
let previousVersion, previousHash
|
||||||
if (!isUniqueEntry)
|
if (!isUniqueEntry) {
|
||||||
({ data: previousVersion, contributors, hash: previousHash } = getIterationDetails(uid))
|
({ data: previousVersion, hash: previousHash = '' } = getIterationDetails(uid))
|
||||||
|
} else {
|
||||||
|
previousHash = ''
|
||||||
|
}
|
||||||
if (previousHash !== hash) {
|
if (previousHash !== hash) {
|
||||||
|
const timestamp = Date.now()
|
||||||
const entry = {
|
const entry = {
|
||||||
section: contentCard.closest('.article-section').dataset.sectionId,
|
section: sectionID,
|
||||||
origin: isUniqueEntry ? floCrypto.randString(16, true) : uid,
|
origin: isUniqueEntry ? floCrypto.randString(16, true) : uid,
|
||||||
data: isUniqueEntry ? clean : getDiff(previousVersion, clean),
|
data: isUniqueEntry ? clean : getDiff(previousVersion, clean),
|
||||||
hash
|
hash
|
||||||
@ -759,21 +771,26 @@
|
|||||||
floCloudAPI.sendGeneralData(entry, `${currentArticle.id}_gd`)
|
floCloudAPI.sendGeneralData(entry, `${currentArticle.id}_gd`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log(res)
|
console.log(res)
|
||||||
|
e.target.closest('.submit-entry').classList.add('hide-completely')
|
||||||
notify('sent data', 'success')
|
notify('sent data', 'success')
|
||||||
if (isUniqueEntry)
|
if (isUniqueEntry) {
|
||||||
contentArea.innerHTML = ''
|
contentArea.innerHTML = ''
|
||||||
else {
|
currentArticle.sections[sectionID].uniqueEntries.push(entry.origin)
|
||||||
|
currentArticle.uniqueEntries[entry.origin] = { iterations: [{ ...entry, timestamp }] }
|
||||||
|
parentSection.firstElementChild.after(render.contentCard(entry.origin))
|
||||||
|
} else {
|
||||||
|
currentArticle.uniqueEntries[entry.origin].iterations.push({ ...entry, timestamp })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
notify("Duplicate entry!", 'error')
|
notify("Duplicate entry!", 'error')
|
||||||
}
|
}
|
||||||
} else if (e.target.closest('.version-history-button')) {
|
} else if (e.target.closest('.version-history-button')) {
|
||||||
if (isHistoryPanelOpen)
|
const entryUid = e.target.closest('.content-card').dataset.uid
|
||||||
|
if (versionHistory.isOpen && entryUid === versionHistory.entryUid)
|
||||||
hideVersionHistory()
|
hideVersionHistory()
|
||||||
else
|
else
|
||||||
showVersionHistory(e.target.closest('.content-card').dataset.uid)
|
showVersionHistory(entryUid)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
getRef('article_wrapper').addEventListener("paste", e => {
|
getRef('article_wrapper').addEventListener("paste", e => {
|
||||||
@ -786,11 +803,28 @@
|
|||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
})
|
})
|
||||||
|
getRef('current_article_title').addEventListener("change", e => {
|
||||||
|
floGlobals.appObjects.cc.articleList[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("change", e => {
|
||||||
|
if (e.target.classList.contains('heading')) {
|
||||||
|
floGlobals.appObjects[currentArticle.id].sections[e.target.dataset.index].title = e.target.value.trim()
|
||||||
|
floCloudAPI.updateObjectData(currentArticle.id)
|
||||||
|
.then((res) => {
|
||||||
|
notify('Updated heading', 'success')
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
})
|
||||||
getRef('article_wrapper').addEventListener("focusin", e => {
|
getRef('article_wrapper').addEventListener("focusin", e => {
|
||||||
if (e.target.closest('.content__area')) {
|
if (e.target.closest('.content__area')) {
|
||||||
document.addEventListener('selectionchange', detectFormatting)
|
|
||||||
|
|
||||||
const target = e.target.closest('.content__area')
|
const target = e.target.closest('.content__area')
|
||||||
|
document.addEventListener('selectionchange', detectFormatting)
|
||||||
if (target.childNodes[0] === undefined) {
|
if (target.childNodes[0] === undefined) {
|
||||||
target.innerHTML = `<p><br/></p>`
|
target.innerHTML = `<p><br/></p>`
|
||||||
}
|
}
|
||||||
@ -799,17 +833,9 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
getRef('article_wrapper').addEventListener("dblclick", e => {
|
|
||||||
if (e.target.closest('.heading')) {
|
|
||||||
const target = e.target.closest('.heading')
|
|
||||||
if (!target.isContentEditable) {
|
|
||||||
target.contentEditable = true
|
|
||||||
target.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
getRef('article_wrapper').addEventListener("focusout", e => {
|
getRef('article_wrapper').addEventListener("focusout", e => {
|
||||||
if (e.target.closest('.content__area')) {
|
if (e.target.closest('.content__area')) {
|
||||||
|
const target = e.target.closest('.content__area')
|
||||||
normalizeText(e.target.closest('.content__area'))
|
normalizeText(e.target.closest('.content__area'))
|
||||||
document.removeEventListener('selectionchange', detectFormatting)
|
document.removeEventListener('selectionchange', detectFormatting)
|
||||||
const selection = window.getSelection()
|
const selection = window.getSelection()
|
||||||
@ -817,12 +843,6 @@
|
|||||||
getRef('text_toolbar').classList.add('hide-completely')
|
getRef('text_toolbar').classList.add('hide-completely')
|
||||||
childObserver.disconnect()
|
childObserver.disconnect()
|
||||||
}
|
}
|
||||||
} else if (e.target.closest('.heading')) {
|
|
||||||
const target = e.target.closest('.heading')
|
|
||||||
if (target.isContentEditable) {
|
|
||||||
floGlobals.appObjects[currentArticle.id].sections[target.dataset.index].title = target.textContent.trim()
|
|
||||||
target.contentEditable = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const childObserver = new MutationObserver((mutations, observer) => {
|
const childObserver = new MutationObserver((mutations, observer) => {
|
||||||
@ -846,17 +866,38 @@
|
|||||||
<script>
|
<script>
|
||||||
let currentArticle = {}
|
let currentArticle = {}
|
||||||
const render = {
|
const render = {
|
||||||
section(sectionID, { title, uniqueEntries }, index) {
|
article(id) {
|
||||||
const section = getRef('section_template').content.cloneNode(true)
|
currentArticle.id = id
|
||||||
|
parseArticleData()
|
||||||
|
const { title } = floGlobals.appObjects.cc.articleList[id]
|
||||||
|
const { writer, sections } = currentArticle
|
||||||
const frag = document.createDocumentFragment()
|
const frag = document.createDocumentFragment()
|
||||||
section.children[0].dataset.index = index
|
let index = 0
|
||||||
section.children[1].dataset.sectionId = sectionID
|
for (const sectionID in sections) {
|
||||||
section.querySelector('.heading').textContent = title
|
frag.append(render.section(sectionID, sections[sectionID], index))
|
||||||
currentArticle.sections[sectionID].uniqueEntries.forEach(entry => {
|
index += 1
|
||||||
frag.append(render.contentCard(entry))
|
}
|
||||||
|
if (!floGlobals.subAdmins.includes(myFloID)) {
|
||||||
|
getRef('current_article_title').setAttribute('disabled', '')
|
||||||
|
}
|
||||||
|
getRef('current_article_title').value = title
|
||||||
|
getRef('article_wrapper').innerHTML = ''
|
||||||
|
getRef('article_wrapper').append(frag)
|
||||||
|
},
|
||||||
|
articleLink(uid, details, isDefaultArticle) {
|
||||||
|
const { timestamp, title } = details
|
||||||
|
const link = createElement('a', {
|
||||||
|
textContent: title,
|
||||||
|
attributes: { href: `#/home?articleID=${uid}` },
|
||||||
|
className: 'article-link flex interact'
|
||||||
})
|
})
|
||||||
section.querySelector('.content-card-container').append(frag)
|
if (isDefaultArticle) {
|
||||||
return section
|
link.append(createElement('span', {
|
||||||
|
className: 'default-article',
|
||||||
|
textContent: 'Active'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return link
|
||||||
},
|
},
|
||||||
contentCard(id, version = 0) {
|
contentCard(id, version = 0) {
|
||||||
const clone = getRef('content_card_template').content.cloneNode(true).firstElementChild;
|
const clone = getRef('content_card_template').content.cloneNode(true).firstElementChild;
|
||||||
@ -870,21 +911,6 @@
|
|||||||
// clone.querySelector('.content__score').textContent = score;
|
// clone.querySelector('.content__score').textContent = score;
|
||||||
return clone
|
return clone
|
||||||
},
|
},
|
||||||
article(id) {
|
|
||||||
currentArticle.id = id
|
|
||||||
parseArticleData()
|
|
||||||
const { title } = floGlobals.appObjects.cc.articleList[id]
|
|
||||||
const { writer, sections } = currentArticle
|
|
||||||
const frag = document.createDocumentFragment()
|
|
||||||
let index = 0
|
|
||||||
for (const sectionID in sections) {
|
|
||||||
frag.append(render.section(sectionID, sections[sectionID], index))
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
getRef('current_article_name').textContent = title
|
|
||||||
getRef('article_wrapper').innerHTML = ''
|
|
||||||
getRef('article_wrapper').append(frag)
|
|
||||||
},
|
|
||||||
historyEntry(details, oldText) {
|
historyEntry(details, oldText) {
|
||||||
const { editor, timestamp, data } = details
|
const { editor, timestamp, data } = details
|
||||||
const clone = getRef('history_entry_template').content.cloneNode(true).firstElementChild;
|
const clone = getRef('history_entry_template').content.cloneNode(true).firstElementChild;
|
||||||
@ -905,15 +931,34 @@
|
|||||||
for (i = index + 1; (changed[i + 1] && changed[i + 1].hasOwnProperty('added') && changed[i + 1].added === type); i++) {
|
for (i = index + 1; (changed[i + 1] && changed[i + 1].hasOwnProperty('added') && changed[i + 1].added === type); i++) {
|
||||||
consecutiveWords.push(changed[i].content)
|
consecutiveWords.push(changed[i].content)
|
||||||
}
|
}
|
||||||
changed.splice(index, i - index)
|
if (i - index > 1)
|
||||||
console.log(consecutiveWords)
|
changed.splice((index + 1), (i - index - 1))
|
||||||
return `<span class="${type ? 'added' : 'removed'}">${consecutiveWords.join(' ')}</span>`
|
return `<span class="${type ? 'added' : 'removed'}">${consecutiveWords.join(' ')}</span>`
|
||||||
} else return word
|
} else return word
|
||||||
})
|
})
|
||||||
clone.querySelector('.entry__changes').innerHTML = final.join(' ')
|
clone.querySelector('.entry__changes').innerHTML = DOMPurify.sanitize(final.join(' '))
|
||||||
|
} else {
|
||||||
|
clone.querySelector('.entry__changes').innerHTML = DOMPurify.sanitize(data)
|
||||||
}
|
}
|
||||||
return clone
|
return clone
|
||||||
}
|
},
|
||||||
|
section(sectionID, { title, uniqueEntries }, index) {
|
||||||
|
const section = getRef('section_template').content.cloneNode(true)
|
||||||
|
const frag = document.createDocumentFragment()
|
||||||
|
section.children[0].dataset.index = index
|
||||||
|
section.children[1].dataset.sectionId = sectionID
|
||||||
|
if (floGlobals.subAdmins.includes(myFloID)) {
|
||||||
|
section.querySelector('.content-card--empty').remove()
|
||||||
|
} else {
|
||||||
|
section.querySelector('.heading').setAttribute('disabled', '')
|
||||||
|
}
|
||||||
|
section.querySelector('.heading').setAttribute('value', title)
|
||||||
|
currentArticle.sections[sectionID].uniqueEntries.forEach(entry => {
|
||||||
|
frag.append(render.contentCard(entry))
|
||||||
|
})
|
||||||
|
section.querySelector('.article-section').append(frag)
|
||||||
|
return section
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArticleData() {
|
function parseArticleData() {
|
||||||
@ -950,6 +995,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderArticleList() {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
getRef('article_list').append(frag)
|
||||||
|
}
|
||||||
|
|
||||||
function getIterationDetails(uid, targetIndex) {
|
function getIterationDetails(uid, targetIndex) {
|
||||||
let merged
|
let merged
|
||||||
const contributors = new Set()
|
const contributors = new Set()
|
||||||
@ -965,47 +1021,34 @@
|
|||||||
hash: currentArticle.uniqueEntries[uid].iterations[limit].hash
|
hash: currentArticle.uniqueEntries[uid].iterations[limit].hash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const versionHistory = {
|
||||||
let isHistoryPanelOpen = false
|
isOpen: false,
|
||||||
|
entryUid: ''
|
||||||
|
}
|
||||||
function showVersionHistory(uid) {
|
function showVersionHistory(uid) {
|
||||||
|
versionHistory.entryUid = uid
|
||||||
const { iterations } = currentArticle.uniqueEntries[uid]
|
const { iterations } = currentArticle.uniqueEntries[uid]
|
||||||
const frag = document.createDocumentFragment()
|
const frag = document.createDocumentFragment()
|
||||||
let mergedChanges/* , oldText */
|
let mergedChanges
|
||||||
// oldText = createElement('div', {
|
|
||||||
// innerHTML: mergedChanges
|
|
||||||
// }).textContent
|
|
||||||
iterations.forEach((iter, index) => {
|
iterations.forEach((iter, index) => {
|
||||||
// const tempText = createElement('div', {
|
|
||||||
// innerHTML: tempMergedChanges
|
|
||||||
// }).textContent
|
|
||||||
// const changes = {
|
|
||||||
// diff: getDiff(oldText, tempText),
|
|
||||||
// }
|
|
||||||
// const versions = {
|
|
||||||
// oldText: mergedChanges,
|
|
||||||
// newText: tempMergedChanges
|
|
||||||
// }
|
|
||||||
// oldText = tempText
|
|
||||||
// console.log(mergedChanges, iter.data, tempMergedChanges)
|
|
||||||
frag.prepend(render.historyEntry(iter, mergedChanges))
|
frag.prepend(render.historyEntry(iter, mergedChanges))
|
||||||
mergedChanges = index ? updateString(mergedChanges, iter.data) : iter.data
|
mergedChanges = index ? updateString(mergedChanges, iter.data) : iter.data
|
||||||
// mergedChanges = tempMergedChanges
|
|
||||||
})
|
})
|
||||||
getRef('version_timeline').innerHTML = ''
|
getRef('version_timeline').innerHTML = ''
|
||||||
getRef('version_timeline').append(frag)
|
getRef('version_timeline').append(frag)
|
||||||
if (!isHistoryPanelOpen) {
|
if (!versionHistory.isOpen) {
|
||||||
getRef('version_history_panel').classList.remove('hide-completely')
|
getRef('version_history_panel').classList.remove('hide-completely')
|
||||||
getRef('main_page').classList.add('active-sidebar')
|
getRef('main_page').classList.add('active-sidebar')
|
||||||
isHistoryPanelOpen = true
|
versionHistory.isOpen = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideVersionHistory() {
|
function hideVersionHistory() {
|
||||||
if (isHistoryPanelOpen) {
|
if (versionHistory.isOpen) {
|
||||||
getRef('version_history_panel').classList.add('hide-completely')
|
getRef('version_history_panel').classList.add('hide-completely')
|
||||||
getRef('version_timeline').innerHTML = ''
|
getRef('version_timeline').innerHTML = ''
|
||||||
getRef('main_page').classList.remove('active-sidebar')
|
getRef('main_page').classList.remove('active-sidebar')
|
||||||
isHistoryPanelOpen = false
|
versionHistory.isOpen = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1203,6 +1246,7 @@
|
|||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
render.article(floGlobals.appObjects.cc.defaultArticle)
|
render.article(floGlobals.appObjects.cc.defaultArticle)
|
||||||
|
renderArticleList()
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(result)
|
console.log(result)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user