Feature and performance update

-- Added content scoring with mark feature

-- Improved performance of version history rendering by eliminating unchanged text from history entry
This commit is contained in:
sairaj mote 2021-12-29 23:04:06 +05:30
parent 18d9de223f
commit 0441960eb9
5 changed files with 221 additions and 60 deletions

View File

@ -1835,7 +1835,6 @@ stripSelect.innerHTML = `
--accent-color: #4d2588;
--text-color: 17, 17, 17;
--background-color: 255, 255, 255;
--gap: 0.5rem;
padding: 1rem 0;
}
.hide{
@ -1855,13 +1854,13 @@ stripSelect.innerHTML = `
:host([multiline]) .strip-select{
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
gap: var(--gap, 0.5rem);
overflow: auto hidden;
}
:host(:not([multiline])) .strip-select{
display: grid;
grid-auto-flow: column;
gap: var(--gap);
gap: var(--gap, 0.5rem);
max-width: 100%;
align-items: center;
overflow: auto hidden;
@ -2099,10 +2098,7 @@ stripOption.innerHTML = `
box-sizing: border-box;
}
:host{
--border-radius: 2rem;
--background-color: inherit;
--active-option-color: inherit;
--active-option-background-color: rgba(var(--text-color), .2);
}
.strip-option{
display: flex;
@ -2111,12 +2107,12 @@ stripOption.innerHTML = `
white-space: nowrap;
padding: var(--padding, 0.4rem 0.6rem);
transition: background 0.3s;
border-radius: var(--border-radius);
border-radius: var(--border-radius, 2rem);
-webkit-tap-highlight-color: transparent;
}
:host([active]) .strip-option{
color: var(--active-option-color);
background-color: var(--active-option-background-color);
color: var(--active-option-color, inherit);
background-color: var(--active-background-color, rgba(var(--text-color), 0.06));
}
:host(:focus-within){
outline: none;

View File

@ -479,6 +479,16 @@ strip-option {
font-size: 0.9rem;
}
strip-select {
--gap: 0;
}
strip-option {
font-weight: 500;
--border-radius: 0.3rem;
--active-option-color: var(--accent-color);
}
sm-checkbox {
--height: 1rem;
--width: 1rem;
@ -873,6 +883,34 @@ sm-copy {
border-radius: 0.3rem;
padding: 0.2rem 0.3rem;
color: rgba(var(--text-color), 0.8);
margin-right: 0.5rem;
}
.content__author {
display: grid;
gap: 0.3rem;
grid-template-columns: auto -webkit-max-content;
grid-template-columns: auto max-content;
}
.content__author div:first-of-type {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.content__author div:last-of-type {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
}
.content__score {
font-size: 0.8rem;
margin-left: 0.2rem;
line-height: 100%;
}
.filled-star {
fill: var(--yellow);
}
.actionable-button {

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -417,6 +417,14 @@ sm-option,
strip-option{
font-size: 0.9rem;
}
strip-select{
--gap: 0;
}
strip-option{
font-weight: 500;
--border-radius: 0.3rem;
--active-option-color: var(--accent-color);
}
sm-checkbox {
--height: 1rem;
--width: 1rem;
@ -756,6 +764,28 @@ sm-copy {
border-radius: 0.3rem;
padding: 0.2rem 0.3rem;
color: rgba(var(--text-color), 0.8);
margin-right: 0.5rem;
}
.content__author{
display: grid;
gap: 0.3rem;
grid-template-columns: auto max-content;
div:first-of-type{
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
div:last-of-type{
flex: 1;
}
}
.content__score{
font-size: 0.8rem;
margin-left: 0.2rem;
line-height: 100%;
}
.filled-star{
fill: var(--yellow);
}
.actionable-button {

View File

@ -590,6 +590,30 @@
<sm-button class="danger" onclick="signOut()">Sign out</sm-button>
</section>
</sm-popup>
<sm-popup id="scoring_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"
fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
</svg>
</button>
<h3>Update score</h3>
</header>
<sm-form>
<sm-input id="update_score_field" class="outlined" placeholder="Score" type="number" step="0.1" min="1"
max="100" error-text="Value must be between 1-100" autofocus animate required hiderequired></sm-input>
<div id="inc_section" class="flex">
<button class="button" onclick="incScore(0.1)">+ 0.1</button>
<button class="button" onclick="incScore(1)">+ 1</button>
<button class="button" onclick="incScore(5)">+ 5</button>
<sm-button id="get_new_score" variant="primary" class="justify-right" onclick="updateScore()" disabled>
Update</sm-button>
</div>
</sm-form>
</sm-popup>
<template id="contributor_template">
<div class="contributor grid">
<svg class="icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -622,7 +646,7 @@
<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>
<span class="content__author"></span>
<div class="content__author flex align-center"></div>
</Button>
<sm-checkbox class="content__checkbox" aria-label="Select content"></sm-checkbox>
</div>
@ -637,15 +661,6 @@
d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.25 2.52.77-1.28-3.52-2.09V8z" />
</svg>
</button>
<button title="Give score">
<svg class="icon button__icon--left" xmlns="http://www.w3.org/2000/svg" height="24px"
viewBox="0 0 24 24" width="24px" fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path
d="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>
<span class="content__score">0</span>
</button>
</div>
<button class="submit-entry hide-completely">Submit</button>
</div>
@ -655,7 +670,6 @@
<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 flex align-center">
@ -1215,7 +1229,8 @@
floGlobals.appObjects[uid] = {
public: true,
editors: [],
sections
sections,
preview: {}
}
Promise.all([
floCloudAPI.updateObjectData('cc'),
@ -1310,14 +1325,19 @@
submitButton.disabled = true
floCloudAPI.sendGeneralData(entry, `${floGlobals.currentArticle.id}_gd`)
.then((res) => {
console.log(res)
let genDataVC
// Add result to general data
for (genDataVC in res) {
floGlobals.generalData[`${floGlobals.currentArticle.id}_gd|${floGlobals.adminID}|${floGlobals.application}`][genDataVC] = res[genDataVC]
}
console.log(genDataVC)
submitButton.classList.add('hide-completely')
notify('sent data', 'success')
const iterationData = { ...entry, timestamp, editor: myFloID }
if (isUniqueEntry) {
contentArea.innerHTML = ''
floGlobals.currentArticle.sections[sectionID].uniqueEntries.push(entry.origin)
floGlobals.currentArticle.uniqueEntries[entry.origin] = { iterations: [iterationData] }
floGlobals.currentArticle.uniqueEntries[entry.origin] = { iterations: [genDataVC] }
// Insert new content card based on set filter
const newCard = render.contentCard(entry.origin)
if (getRef('sort_content_list').value === 'time') {
@ -1360,7 +1380,7 @@
if (noOfContributors < 2) {
contentCard.querySelector('.content__author').textContent = `2 Contributors`
}
floGlobals.currentArticle.uniqueEntries[entry.origin].iterations.push(iterationData)
floGlobals.currentArticle.uniqueEntries[entry.origin].iterations.push(genDataVC)
}
})
.catch(err => console.log(err))
@ -1373,7 +1393,7 @@
}
} else if (e.target.closest('.version-history-button')) {
const entryUid = e.target.closest('.content-card').dataset.uid
if (versionHistory.isOpen && entryUid === versionHistory.entryUid)
if (floGlobals.versionHistory.isOpen && entryUid === floGlobals.versionHistory.entryUid)
hideVersionHistory()
else
showVersionHistory(entryUid)
@ -1409,6 +1429,17 @@
})
floGlobals.currentArticle.sections[sectionID].expanded = true
}
} else if (e.target.closest('.score-button') && isSubAdmin) {
floGlobals.versionHistory.currentEntry = e.target.closest('.content-card').dataset.vectorClock
getRef('update_score_field').value = e.target.closest('.score-button').children[1].textContent
showPopup('scoring_popup')
}
})
getRef('version_timeline').addEventListener('click', e => {
if (e.target.closest('.score-button') && isSubAdmin) {
floGlobals.versionHistory.currentEntry = e.target.closest('.history-entry').dataset.vectorClock
getRef('update_score_field').value = e.target.closest('.score-button').children[1].textContent
showPopup('scoring_popup')
}
})
getRef('article_wrapper').addEventListener("paste", e => {
@ -1606,7 +1637,7 @@
}
function sharePreview() {
if (isSubAdmin) {
if (floGlobals.appObjects[pagesData.params.articleID].preview) {
if (floGlobals.appObjects[pagesData.params.articleID]?.preview?.id) {
floGlobals.appObjects[pagesData.params.articleID].preview.content = DOMPurify.sanitize(getRef('preview__body').innerHTML)
} else {
floGlobals.appObjects[pagesData.params.articleID].preview = {
@ -1757,6 +1788,31 @@
function formatDoc(sCmd, sValue) {
document.execCommand(sCmd, false, sValue);
}
function incScore(value) {
let currentScore = parseFloat(getRef('update_score_field').value)
if (!currentScore) {
currentScore = 0
}
if (currentScore + value <= 100) {
currentScore += value;
getRef('update_score_field').value = parseFloat(currentScore.toFixed(1))
}
else {
notify(`You can't give score more than 100.`, 'error')
}
}
function updateScore() {
const newScore = parseFloat(getRef('update_score_field').value)
const mark = {
[floGlobals.versionHistory.currentEntry]: newScore
}
scores[floGlobals.versionHistory.currentEntry] = newScore
document.querySelectorAll(`[data-vector-clock="${floGlobals.versionHistory.currentEntry}"] .content__score`).forEach(scoreElem => scoreElem.textContent = newScore)
floCloudAPI.markApplicationData(mark).then(res => {
notify('Score updated', 'success')
hidePopup()
}).catch(err => notify(err, 'error'))
}
</script>
<script>
floGlobals.currentArticle = {}
@ -1797,6 +1853,28 @@
}
mobileQuery.addListener(handleMobileChange)
handleMobileChange(mobileQuery)
function getScoreElement(score) {
let scoreElement
if (isSubAdmin) {
scoreElement = createElement('button', {
className: 'score-button',
attributes: { 'title': 'Score this content' }
})
} else {
scoreElement = createElement('div', {
className: 'flex align-center',
attributes: { 'title': 'Score' }
})
}
scoreElement.innerHTML = `
${score ?
`<svg class="icon filled-star" 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"/><path d="M0,0h24v24H0V0z" fill="none"/></g><g><path d="M12,17.27L18.18,21l-1.64-7.03L22,9.24l-7.19-0.61L12,2L9.19,8.63L2,9.24l5.46,4.73L5.82,21L12,17.27z"/></g></svg> ` :
`<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0V0z" fill="none" /> <path d="M12 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>`
}
<div class="content__score">${score}</div>
`
return scoreElement
}
const render = {
article(id) {
floGlobals.currentArticle.id = id
@ -1841,32 +1919,30 @@
return link
},
contentCard(id, version = 0) {
const { data, contributors, score = 0 } = getIterationDetails(id)
const { data, contributors, score = 0, vectorClock } = getIterationDetails(id)
const clone = getRef('content_card_template').content.cloneNode(true).firstElementChild;
clone.dataset.uid = id
clone.dataset.vectorClock = vectorClock
if (!isSubAdmin) {
clone.querySelector('.content__area').setAttribute('contentEditable', true)
}
clone.querySelector('.content__area').innerHTML = DOMPurify.sanitize(data)
let noOfContributors = 0
let firstContributor
let latestContributor
for (const contributor in contributors) {
noOfContributors++
if (noOfContributors === 1)
firstContributor = contributor
latestContributor = contributor
}
if (noOfContributors === 1) {
clone.querySelector('.content__author').textContent = firstContributor
} else {
clone.querySelector('.content__author').textContent = `${noOfContributors} contributors`
}
clone.querySelector('.content__score').textContent = score;
clone.querySelector('.content__author').innerHTML = `<div>${latestContributor}</div> ${noOfContributors === 1 ? '' : `<div> and ${noOfContributors - 1} more`}</div>`
clone.querySelector('.content__options').append(getScoreElement(score));
return clone
},
historyEntry(details, oldText) {
const { editor, timestamp, data } = details
const { editor, timestamp, data, score, vectorClock } = details
const clone = getRef('history_entry_template').content.cloneNode(true).firstElementChild;
clone.dataset.vectorClock = vectorClock
clone.querySelector('.entry__time').textContent = getFormattedTime(timestamp)
clone.querySelector('.entry__time').after(getScoreElement(score))
clone.querySelector('.entry__author').textContent = editor
if (Array.isArray(data)) {
const [removedAt, addedWords, addedAt] = data
@ -1892,7 +1968,12 @@
}
} else return word
})
clone.querySelector('.entry__changes').innerHTML = DOMPurify.sanitize(final.join(' '))
clone.querySelector('.entry__changes').innerHTML = DOMPurify.sanitize(final.join(' '));
[...clone.querySelector('.entry__changes').children].forEach(element => {
if (element.tagName === 'P' && !element.querySelector('.added, .removed')) {
element.remove()
}
});
} else {
clone.querySelector('.entry__changes').innerHTML = DOMPurify.sanitize(data)
}
@ -1952,29 +2033,42 @@
})
floGlobals.currentArticle['uniqueEntries'] = {}
for (const key in generalData) {
const { message: { section, data, origin, hash }, senderID } = generalData[key]
const { section, origin } = generalData[key].message
if (!floGlobals.currentArticle.uniqueEntries.hasOwnProperty(origin)) { // check if general data has origin that's already defined
floGlobals.currentArticle.uniqueEntries[origin] = {
iterations: []
}
}
floGlobals.currentArticle.sections[section].uniqueEntries.add(origin)
floGlobals.currentArticle.uniqueEntries[origin]['iterations'].push({
timestamp: generalData[key].time,
data,
editor: senderID,
hash,
score: floCrypto.randInt(0, 100) // to do: get score from mark feature
})
floGlobals.currentArticle.uniqueEntries[origin]['iterations'].push(generalData[key].vectorClock)
}
for (const sectionID in floGlobals.currentArticle.sections) {
floGlobals.currentArticle.sections[sectionID].uniqueEntries = [...floGlobals.currentArticle.sections[sectionID].uniqueEntries].reverse()
}
for (const entry in floGlobals.currentArticle.uniqueEntries) {
floGlobals.currentArticle.uniqueEntries[entry]['iterations'].sort((a, b) => a.timestamp - b.timestamp)
floGlobals.currentArticle.uniqueEntries[entry]['iterations'].sort((a, b) => getGenData(a).timestamp - getGenData(b).timestamp)
}
}
function getGenData(vectorClock) {
const { message: { section, origin, data, hash }, senderID, time } = floGlobals.generalData[`${floGlobals.currentArticle.id}_gd|${floGlobals.adminID}|${floGlobals.application}`][vectorClock]
return {
data,
editor: senderID,
timestamp: time,
vectorClock,
score: getScore(vectorClock)
}
}
const scores = {}
function getScore(vc) {
if (!scores[vc])
scores[vc] = floCrypto.randInt(0, 100)
return scores[vc]
}
let currentOptionsPanel = ''
function toggleOptionsPanel(type) {
const animInOptions = {
@ -2062,7 +2156,7 @@
floGlobals.currentArticle.sections[sectionID].uniqueEntries.sort((a, b) => {
const arrayA = floGlobals.currentArticle.uniqueEntries[a].iterations
const arrayB = floGlobals.currentArticle.uniqueEntries[b].iterations
return arrayB[arrayB.length - 1][sortByScore ? 'score' : 'timestamp'] - arrayA[arrayA.length - 1][sortByScore ? 'score' : 'timestamp']
return getGenData(arrayB[arrayB.length - 1])[sortByScore ? 'score' : 'timestamp'] - getGenData(arrayA[arrayA.length - 1])[sortByScore ? 'score' : 'timestamp']
})
}
for (const sectionID in floGlobals.currentArticle.sections) {
@ -2213,45 +2307,48 @@
const contributors = {}
const limit = targetIndex || floGlobals.currentArticle.uniqueEntries[uid].iterations.length - 1
for (let i = 0; i <= limit; i++) {
const { data, editor, timestamp } = floGlobals.currentArticle.uniqueEntries[uid].iterations[i]
const { data, editor, timestamp } = getGenData(floGlobals.currentArticle.uniqueEntries[uid].iterations[i])
merged = i ? updateString(merged, data) : data
contributors[editor] = timestamp
}
const { hash, score } = getGenData(floGlobals.currentArticle.uniqueEntries[uid].iterations[limit]);
return {
data: merged,
contributors,
hash: floGlobals.currentArticle.uniqueEntries[uid].iterations[limit].hash,
score: floGlobals.currentArticle.uniqueEntries[uid].iterations[limit].score
hash,
score,
vectorClock: floGlobals.currentArticle.uniqueEntries[uid].iterations[limit]
}
}
const versionHistory = {
floGlobals.versionHistory = {
isOpen: false,
entryUid: ''
}
function showVersionHistory(uid) {
versionHistory.entryUid = uid
floGlobals.versionHistory.entryUid = uid
const { iterations } = floGlobals.currentArticle.uniqueEntries[uid]
const frag = document.createDocumentFragment()
let mergedChanges
iterations.forEach((iter, index) => {
iterations.forEach((vectorClock, index) => {
const iter = getGenData(vectorClock)
frag.prepend(render.historyEntry(iter, mergedChanges))
mergedChanges = index ? updateString(mergedChanges, iter.data) : iter.data
})
getRef('version_timeline').innerHTML = ''
getRef('version_timeline').append(frag)
if (!versionHistory.isOpen) {
if (!floGlobals.versionHistory.isOpen) {
getRef('version_history_panel').classList.remove('hide-completely')
getRef('main_page').classList.add('active-sidebar')
versionHistory.isOpen = true
floGlobals.versionHistory.isOpen = true
}
}
function hideVersionHistory() {
if (versionHistory.isOpen) {
if (floGlobals.versionHistory.isOpen) {
getRef('version_history_panel').classList.add('hide-completely')
getRef('version_timeline').innerHTML = ''
getRef('main_page').classList.remove('active-sidebar')
versionHistory.isOpen = false
floGlobals.versionHistory.isOpen = false
}
}