implemented version history feature in UI
This commit is contained in:
parent
ecf86ef764
commit
05480c7462
66
css/main.css
66
css/main.css
@ -548,9 +548,19 @@ sm-checkbox {
|
||||
fill: var(--danger-color);
|
||||
}
|
||||
|
||||
#main_page {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
#article_wrapper {
|
||||
justify-self: center;
|
||||
padding: 1rem 0;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
gap: 1rem 0;
|
||||
}
|
||||
|
||||
@ -574,6 +584,8 @@ sm-checkbox {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.article-section:not(:last-of-type) {
|
||||
margin-bottom: 1.5rem;
|
||||
@ -717,32 +729,24 @@ sm-checkbox {
|
||||
}
|
||||
|
||||
#version_history_panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
width: min(22rem, 100%);
|
||||
width: min(24rem, 100%);
|
||||
background-color: var(--foreground-color);
|
||||
-webkit-box-shadow: -0.5rem 0 1rem rgba(0, 0, 0, 0.1);
|
||||
box-shadow: -0.5rem 0 1rem rgba(0, 0, 0, 0.1);
|
||||
overflow-y: auto;
|
||||
}
|
||||
#version_history_panel > :first-child {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#version_timeline {
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
margin-top: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-entry {
|
||||
padding: 1rem;
|
||||
border-radius: 0.3rem;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
.history-entry:not(:last-of-type) {
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: thin solid rgba(var(--text-color), 0.3);
|
||||
}
|
||||
.history-entry:last-of-type::before {
|
||||
content: "CREATED";
|
||||
@ -753,8 +757,9 @@ sm-checkbox {
|
||||
justify-self: flex-start;
|
||||
font-weight: 500;
|
||||
padding: 0.2rem 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
background-color: rgba(var(--text-color), 0.06);
|
||||
font-size: 0.7rem;
|
||||
border-radius: 0.2rem;
|
||||
border: solid thin rgba(var(--text-color), 0.5);
|
||||
}
|
||||
|
||||
.entry__time,
|
||||
@ -763,6 +768,13 @@ sm-checkbox {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.entry__changes .added {
|
||||
background-color: #00e67650;
|
||||
}
|
||||
.entry__changes .removed {
|
||||
background-color: #ff3a4a50;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 40rem) and (any-hover: none) {
|
||||
.cancel-order span {
|
||||
display: none;
|
||||
@ -799,6 +811,7 @@ sm-checkbox {
|
||||
}
|
||||
|
||||
.popup__header {
|
||||
grid-column: 1/-1;
|
||||
padding: 1rem 1.5rem 0 0.5rem;
|
||||
}
|
||||
|
||||
@ -813,6 +826,17 @@ sm-checkbox {
|
||||
.hide-on-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#main_page.active-sidebar {
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-columns: minmax(0, 1fr) 24rem;
|
||||
}
|
||||
#main_page.active-sidebar #article_wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@media (any-hover: hover) {
|
||||
::-webkit-scrollbar {
|
||||
|
||||
2
css/main.min.css
vendored
2
css/main.min.css
vendored
File diff suppressed because one or more lines are too long
@ -395,9 +395,6 @@ button:active,
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#main_page {
|
||||
}
|
||||
.logo {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
@ -484,9 +481,14 @@ sm-checkbox {
|
||||
fill: var(--danger-color);
|
||||
}
|
||||
|
||||
#main_page {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
#article_wrapper {
|
||||
justify-self: center;
|
||||
padding: 1rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
gap: 1rem 0;
|
||||
}
|
||||
|
||||
@ -506,6 +508,7 @@ sm-checkbox {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
flex-shrink: 0;
|
||||
&:not(:last-of-type) {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@ -629,26 +632,24 @@ sm-checkbox {
|
||||
}
|
||||
|
||||
#version_history_panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
width: min(22rem, 100%);
|
||||
width: min(24rem, 100%);
|
||||
background-color: var(--foreground-color);
|
||||
box-shadow: -0.5rem 0 1rem rgba(0, 0, 0, 0.1);
|
||||
overflow-y: auto;
|
||||
& > :first-child {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
#version_timeline {
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
margin-top: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.history-entry {
|
||||
padding: 1rem;
|
||||
border-radius: 0.3rem;
|
||||
user-select: none;
|
||||
&:not(:last-of-type) {
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: thin solid rgba(var(--text-color), 0.3);
|
||||
}
|
||||
&:last-of-type::before {
|
||||
content: "CREATED";
|
||||
letter-spacing: 0.03em;
|
||||
@ -656,8 +657,9 @@ sm-checkbox {
|
||||
justify-self: flex-start;
|
||||
font-weight: 500;
|
||||
padding: 0.2rem 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
background-color: rgba(var(--text-color), 0.06);
|
||||
font-size: 0.7rem;
|
||||
border-radius: 0.2rem;
|
||||
border: solid thin rgba(var(--text-color), 0.5);
|
||||
}
|
||||
}
|
||||
.entry__time,
|
||||
@ -665,6 +667,14 @@ sm-checkbox {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.entry__changes {
|
||||
.added {
|
||||
background-color: #00e67650;
|
||||
}
|
||||
.removed {
|
||||
background-color: #ff3a4a50;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 40rem) and (any-hover: none) {
|
||||
.cancel-order {
|
||||
@ -701,6 +711,7 @@ sm-checkbox {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.popup__header {
|
||||
grid-column: 1/-1;
|
||||
padding: 1rem 1.5rem 0 0.5rem;
|
||||
}
|
||||
#confirmation_popup {
|
||||
@ -712,6 +723,18 @@ sm-checkbox {
|
||||
.hide-on-desktop {
|
||||
display: none;
|
||||
}
|
||||
#main_page {
|
||||
&.active-sidebar {
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-columns: minmax(0, 1fr) 24rem;
|
||||
#article_wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (any-hover: hover) {
|
||||
::-webkit-scrollbar {
|
||||
|
||||
118
index.html
118
index.html
@ -133,8 +133,8 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="article_wrapper" class="grid page-layout"></div>
|
||||
<section class="grid page-layout">
|
||||
<div id="article_wrapper"></div>
|
||||
<!-- <div class="grid page-layout">
|
||||
<div id="action_button_group" class="flex align-center gap-0-5">
|
||||
<span>
|
||||
<b>
|
||||
@ -142,8 +142,8 @@
|
||||
</b>
|
||||
</span>
|
||||
<button class="actionable-button" title="Add paragraph">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
||||
fill="#000000">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
||||
width="24px" fill="#000000">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M14 17H4v2h10v-2zm6-8H4v2h16V9zM4 15h16v-2H4v2zM4 5v2h16V5H4z" />
|
||||
</svg>
|
||||
@ -152,7 +152,7 @@
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div> -->
|
||||
<aside id="version_history_panel" class="flex direction-column hide-completely">
|
||||
<div class="flex align-center space-between">
|
||||
<div class="flex align-center">
|
||||
@ -173,7 +173,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<ul id="version_timeline" class="flex direction-column gap-0-5"></ul>
|
||||
<ul id="version_timeline" class="flex direction-column gap-1-5"></ul>
|
||||
</aside>
|
||||
</article>
|
||||
<sm-popup id="article_list_popup">
|
||||
@ -270,7 +270,7 @@
|
||||
<p><br></p>
|
||||
</template>
|
||||
<template id="history_entry_template">
|
||||
<li class="history-entry interact grid gap-0-5">
|
||||
<li class="history-entry grid gap-0-5">
|
||||
<div class="flex align-center space-between">
|
||||
<time class="entry__time"></time>
|
||||
<span class="entry__score"></span>
|
||||
@ -279,6 +279,7 @@
|
||||
<div class="label">Author</div>
|
||||
<span class="entry__author breakable"></span>
|
||||
</div>
|
||||
<div class="entry__changes"></div>
|
||||
</li>
|
||||
</template>
|
||||
<script id="ui_utils">
|
||||
@ -673,8 +674,16 @@
|
||||
returns newStr
|
||||
*/
|
||||
|
||||
function splitHTML(string) {
|
||||
const el = createElement('div', {
|
||||
innerHTML: string
|
||||
});
|
||||
return Array.from(el.childNodes).map(e => (e.outerHTML || e.innerHTML || e.nodeValue).split(' ').filter(t => t)).flat();
|
||||
}
|
||||
|
||||
function getDiff(oldStr, newStr) {
|
||||
let d = patienceDiff(oldStr.split(" "), newStr.split(" "), true);
|
||||
console.log(splitHTML(oldStr), splitHTML(newStr))
|
||||
let d = patienceDiff(splitHTML(oldStr), splitHTML(newStr), true);
|
||||
return [d.aMoveIndex, d.bMove, d.bMoveIndex]
|
||||
}
|
||||
|
||||
@ -732,16 +741,17 @@
|
||||
contentArea.querySelectorAll('[style=""]').forEach((el) => {
|
||||
el.removeAttribute('style')
|
||||
})
|
||||
const clean = DOMPurify.sanitize(contentArea.innerHTML);
|
||||
const clean = DOMPurify.sanitize(contentArea.innerHTML.split('\n').map(v => v.trim()).filter(v => v));
|
||||
if (clean.trim() === '') return
|
||||
let previousVersion, contributors
|
||||
if (!isUniqueEntry)
|
||||
({ data: previousVersion, contributors } = getIterationDetails(uid))
|
||||
const entry = {
|
||||
section: contentCard.closest('.article-section').dataset.id,
|
||||
section: contentCard.closest('.article-section').dataset.sectionId,
|
||||
origin: isUniqueEntry ? floCrypto.randString(16, true) : uid,
|
||||
data: isUniqueEntry ? clean : getDiff(previousVersion, clean),
|
||||
}
|
||||
console.log(entry)
|
||||
floCloudAPI.sendGeneralData(entry, `${currentArticle.id}_gd`)
|
||||
.then((res) => {
|
||||
console.log(res)
|
||||
@ -750,7 +760,10 @@
|
||||
contentArea.innerHTML = ''
|
||||
})
|
||||
} else if (e.target.closest('.version-history-button')) {
|
||||
showVersionHistory(e.target.closest('.content-card').dataset.uid)
|
||||
if (isHistoryPanelOpen)
|
||||
hideVersionHistory()
|
||||
else
|
||||
showVersionHistory(e.target.closest('.content-card').dataset.uid)
|
||||
}
|
||||
})
|
||||
getRef('article_wrapper').addEventListener("paste", e => {
|
||||
@ -776,6 +789,15 @@
|
||||
})
|
||||
}
|
||||
})
|
||||
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 => {
|
||||
if (e.target.closest('.content__area')) {
|
||||
document.removeEventListener('selectionchange', detectFormatting)
|
||||
@ -785,6 +807,12 @@
|
||||
if (!e.relatedTarget?.closest('#text_toolbar')) {
|
||||
getRef('text_toolbar').classList.add('hide-completely')
|
||||
}
|
||||
} 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) => {
|
||||
@ -808,10 +836,11 @@
|
||||
<script>
|
||||
let currentArticle = {}
|
||||
const render = {
|
||||
section(sectionID, { title, uniqueEntries }) {
|
||||
section(sectionID, { title, uniqueEntries }, index) {
|
||||
const section = getRef('section_template').content.cloneNode(true)
|
||||
const frag = document.createDocumentFragment()
|
||||
section.children[1].dataset.id = sectionID
|
||||
section.children[0].dataset.index = index
|
||||
section.children[1].dataset.sectionId = sectionID
|
||||
section.querySelector('.heading').textContent = title
|
||||
currentArticle.sections[sectionID].uniqueEntries.forEach(entry => {
|
||||
frag.append(render.contentCard(entry))
|
||||
@ -837,18 +866,33 @@
|
||||
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]))
|
||||
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) {
|
||||
const { editor, timestamp } = details
|
||||
historyEntry(details, oldText) {
|
||||
const { editor, timestamp, data } = details
|
||||
const clone = getRef('history_entry_template').content.cloneNode(true).firstElementChild;
|
||||
clone.querySelector('.entry__time').textContent = getFormattedTime(timestamp)
|
||||
clone.querySelector('.entry__author').textContent = editor
|
||||
if (Array.isArray(data)) {
|
||||
console.log(data)
|
||||
const [removedAt, addedWords, addedAt] = data
|
||||
const changed = oldText.split(' ')
|
||||
let firstAddedPlace
|
||||
let startIndex, endIndex
|
||||
let addedNodes
|
||||
addedAt.forEach((place, index) => {
|
||||
changed.splice(place, 0, `<span class="added">${addedWords[index]}</span>`)
|
||||
})
|
||||
removedAt.forEach(place => changed[place] = `<span class="removed">${changed[place]}</span>`)
|
||||
clone.querySelector('.entry__changes').innerHTML = changed.join(' ')
|
||||
}
|
||||
return clone
|
||||
}
|
||||
}
|
||||
@ -866,7 +910,7 @@
|
||||
currentArticle['uniqueEntries'] = {}
|
||||
for (const key in generalData) {
|
||||
const { message: { section, data, origin }, senderID } = generalData[key]
|
||||
if (!currentArticle.uniqueEntries.hasOwnProperty(origin)) {
|
||||
if (!currentArticle.uniqueEntries.hasOwnProperty(origin)) { // check if gen data has origin that's already defined
|
||||
currentArticle.uniqueEntries[origin] = {
|
||||
iterations: []
|
||||
}
|
||||
@ -878,6 +922,9 @@
|
||||
editor: senderID
|
||||
})
|
||||
}
|
||||
for (const sectionID in currentArticle.sections) {
|
||||
currentArticle.sections[sectionID].uniqueEntries = [...currentArticle.sections[sectionID].uniqueEntries].reverse()
|
||||
}
|
||||
for (const entry in currentArticle.uniqueEntries) {
|
||||
currentArticle.uniqueEntries[entry]['iterations'].sort((a, b) => a.timestamp - b.timestamp)
|
||||
}
|
||||
@ -898,20 +945,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
let isHistoryPanelOpen = false
|
||||
function showVersionHistory(uid) {
|
||||
const { iterations } = currentArticle.uniqueEntries[uid]
|
||||
const frag = document.createDocumentFragment()
|
||||
iterations.forEach(iter => {
|
||||
frag.prepend(render.historyEntry(iter))
|
||||
let mergedChanges/* , oldText */
|
||||
// oldText = createElement('div', {
|
||||
// innerHTML: mergedChanges
|
||||
// }).textContent
|
||||
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))
|
||||
mergedChanges = index ? updateString(mergedChanges, iter.data) : iter.data
|
||||
// mergedChanges = tempMergedChanges
|
||||
})
|
||||
getRef('version_timeline').innerHTML = ''
|
||||
getRef('version_timeline').append(frag)
|
||||
getRef('version_history_panel').classList.remove('hide-completely')
|
||||
if (!isHistoryPanelOpen) {
|
||||
getRef('version_history_panel').classList.remove('hide-completely')
|
||||
getRef('main_page').classList.add('active-sidebar')
|
||||
isHistoryPanelOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
function hideVersionHistory() {
|
||||
getRef('version_history_panel').classList.add('hide-completely')
|
||||
getRef('version_timeline').innerHTML = ''
|
||||
if (isHistoryPanelOpen) {
|
||||
getRef('version_history_panel').classList.add('hide-completely')
|
||||
getRef('version_timeline').innerHTML = ''
|
||||
getRef('main_page').classList.remove('active-sidebar')
|
||||
isHistoryPanelOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
const splitAt = (string, index) => [string.slice(0, index), string.slice(index)]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user