+ Giant update to multiple users editing notes.
- Edits are done per DOM element, making diffs smaller and faster - Multiple users can now edit the same note witheout it turning into a gaint mess - Caret position is saved better and wont jump around as much + Removed Active sessions text
This commit is contained in:
parent
c61f0c0198
commit
b5ef64485f
@ -592,6 +592,15 @@ padding-right: 10px;
|
||||
color: var(--main-accent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Remove indent line on mobile */
|
||||
.note-card-text > ol > ol,
|
||||
.squire-box > ol > ol,
|
||||
.note-card-text > ul > ul,
|
||||
.squire-box > ul > ul
|
||||
{
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -173,7 +173,8 @@
|
||||
<span class="status-menu" v-on:click=" hash=0; save()">
|
||||
|
||||
<span v-if="diffsApplied > 0">
|
||||
+{{ diffsApplied }} Unsaved Changes
|
||||
<i class="blue wave square icon"></i>
|
||||
+{{ diffsApplied }}
|
||||
</span>
|
||||
|
||||
<span v-if="usersOnNote > 1" :data-tooltip="`Viewers`" data-position="left center">
|
||||
@ -360,6 +361,8 @@
|
||||
|
||||
import SquireButtonFunctions from '@/mixins/SquireButtonFunctions.js'
|
||||
|
||||
let rawNoteText = '' // Used for comparing and generating diffs
|
||||
|
||||
export default {
|
||||
name: 'NoteInputPanel',
|
||||
props: [ 'noteid', 'position', 'openMenu', 'urlData', 'openNotes'],
|
||||
@ -390,7 +393,6 @@
|
||||
created: '',
|
||||
updated: '',
|
||||
shareUsername: null,
|
||||
// diffNoteText: '',
|
||||
statusText: 'saved',
|
||||
lastNoteHash: null,
|
||||
saveDebounce: null, //Prevent save from being called numerous times quickly
|
||||
@ -428,6 +430,7 @@
|
||||
//Used to restore caret position
|
||||
lastRange: null,
|
||||
startOffset: 0,
|
||||
childIndex: null,
|
||||
|
||||
//Tag Display
|
||||
allTags: [],
|
||||
@ -570,33 +573,31 @@
|
||||
|
||||
})
|
||||
},
|
||||
initSquire(){
|
||||
initSquireEvents(){
|
||||
|
||||
//Set up squire and load note text
|
||||
this.setText(this.noteText)
|
||||
|
||||
// Use squire box HTML for diff/patch changes
|
||||
rawNoteText = document.getElementById('squire-id').innerHTML
|
||||
|
||||
//focus on open, not on mobile, it causes the keyboard to pop up, thats annoying
|
||||
if(!this.$store.getters.getIsUserOnMobile){
|
||||
this.editor.focus()
|
||||
this.editor.moveCursorToEnd()
|
||||
}
|
||||
|
||||
//Set up websockets after squire is set up
|
||||
setTimeout(() => {
|
||||
this.setupWebSockets()
|
||||
}, 500)
|
||||
|
||||
this.editor.addEventListener('cursor', e => {
|
||||
|
||||
//Save range to replace cursor if someone else makes an update
|
||||
this.lastRange = e.range
|
||||
this.startOffset = parseInt(e.range.startOffset)
|
||||
return
|
||||
this.saveCaretPosition(e)
|
||||
})
|
||||
|
||||
//Change button states on editor when element is active
|
||||
//eg; Bold button turns green when on bold text
|
||||
this.editor.addEventListener('pathChange', e => this.pathChangeEvent(e))
|
||||
this.editor.addEventListener('pathChange', e => {
|
||||
this.pathChangeEvent(e)
|
||||
this.diffText(e)
|
||||
})
|
||||
|
||||
//Click Event - Open links when clicked in editor or toggle checks
|
||||
this.editor.addEventListener('click', e => {
|
||||
@ -681,10 +682,17 @@
|
||||
})
|
||||
})
|
||||
|
||||
//Bind event handlers
|
||||
this.editor.addEventListener('keyup', event => {
|
||||
|
||||
this.onKeyup(event)
|
||||
this.diffText(event)
|
||||
})
|
||||
|
||||
this.editor.addEventListener('focus', e => {
|
||||
// this.diffText(e)
|
||||
})
|
||||
|
||||
this.editor.addEventListener('blur', e => {
|
||||
this.diffText(e)
|
||||
})
|
||||
|
||||
// this.editor.addEventListener("dragstart", e => {
|
||||
@ -692,12 +700,6 @@
|
||||
// console.log(e)
|
||||
// if(){}
|
||||
// });
|
||||
|
||||
//Show and hide additional toolbars
|
||||
// this.editor.addEventListener('focus', e => {
|
||||
// })
|
||||
// this.editor.addEventListener('blur', e => {
|
||||
// })
|
||||
},
|
||||
openEditAttachment(){
|
||||
|
||||
@ -760,13 +762,18 @@
|
||||
//Setup all responsive vue data
|
||||
this.setupLoadedNoteData(response)
|
||||
|
||||
this.loading = false
|
||||
|
||||
this.$nextTick(() => {
|
||||
|
||||
//Adjust note title size after load
|
||||
this.titleResize()
|
||||
this.initSquire()
|
||||
this.initSquireEvents()
|
||||
|
||||
//Set up websockets after squire is set up
|
||||
setTimeout(() => {
|
||||
this.initWebsocketEvents()
|
||||
|
||||
this.loading = false
|
||||
}, 500)
|
||||
})
|
||||
|
||||
})
|
||||
@ -786,14 +793,11 @@
|
||||
this.created = response.data.created
|
||||
this.updated = response.data.updated
|
||||
this.lastInteractionTimestamp = +new Date
|
||||
this.noteTitle = ''
|
||||
if(response.data.title){
|
||||
this.noteTitle = response.data.title
|
||||
}
|
||||
this.noteTitle = response.data.title || ''
|
||||
|
||||
this.noteText = response.data.text
|
||||
this.lastNoteHash = this.hashString( response.data.text )
|
||||
// this.diffNoteText = response.data.text
|
||||
|
||||
|
||||
//Setup note tags
|
||||
this.allTags = response.data.tags ? response.data.tags.split(','):[]
|
||||
@ -811,77 +815,167 @@
|
||||
|
||||
return true
|
||||
|
||||
},
|
||||
generateSelector(el){
|
||||
|
||||
if (!(el instanceof Element))
|
||||
return;
|
||||
|
||||
var path = [];
|
||||
while (el.nodeType === Node.ELEMENT_NODE) {
|
||||
var selector = el.nodeName.toLowerCase();
|
||||
if (el.id) {
|
||||
selector += '#' + el.id;
|
||||
path.unshift(selector);
|
||||
break;
|
||||
} else {
|
||||
var sib = el, nth = 1;
|
||||
while (sib = sib.previousElementSibling) {
|
||||
if (sib.nodeName.toLowerCase() == selector)
|
||||
nth++;
|
||||
}
|
||||
if (nth != 1)
|
||||
selector += ":nth-of-type("+nth+")";
|
||||
}
|
||||
path.unshift(selector);
|
||||
el = el.parentNode;
|
||||
}
|
||||
return path.join(" > ");
|
||||
},
|
||||
//Called on squire event for keyup
|
||||
diffText(event){
|
||||
// console.log(event.type)
|
||||
|
||||
//Diff the changed lines only
|
||||
const diffEvents = ['keyup','pathChange', 'click']
|
||||
|
||||
let oldText = this.noteText
|
||||
// let newText = this.getText()
|
||||
let newText = document.getElementById('squire-id').innerHTML
|
||||
// only process changes on certain events
|
||||
if( !diffEvents.includes(event?.type) ){
|
||||
return
|
||||
}
|
||||
|
||||
const diff = dmp.diff_main(oldText, newText)
|
||||
// dmp.diff_cleanupSemantic(diff)
|
||||
const patch_list = dmp.patch_make(oldText, newText, diff);
|
||||
const patch_text = dmp.patch_toText(patch_list);
|
||||
clearTimeout(this.diffTextTimeout)
|
||||
this.diffTextTimeout = setTimeout(() => {
|
||||
|
||||
if(patch_text == ''){ return }
|
||||
// Current Editor Text
|
||||
const liveEditorElm = document.getElementById('squire-id')
|
||||
|
||||
//Save computed diff text
|
||||
this.noteText = newText
|
||||
// virtual element for selecting div
|
||||
let virtualEditorElm = document.createElement('div')
|
||||
virtualEditorElm.innerHTML = rawNoteText
|
||||
|
||||
// element at cursor
|
||||
const elmAtCaret = window.getSelection().getRangeAt(0).startContainer.parentNode
|
||||
|
||||
// Remove beginngin selector from path, make it more generic
|
||||
const path = this.generateSelector(elmAtCaret).replace('div#squire-id > ','')
|
||||
let workingPath = ''
|
||||
|
||||
// default to entire note text, select down if path
|
||||
let selectedDivText = virtualEditorElm
|
||||
let newSelectedDivText = liveEditorElm
|
||||
|
||||
if( path != ''){
|
||||
|
||||
const pathParts = path.split(' > ')
|
||||
let testedPathParts = []
|
||||
let workingPathParts = []
|
||||
|
||||
for (var i = 0; i < pathParts.length; i++) {
|
||||
|
||||
testedPathParts.push(pathParts[i])
|
||||
let currentTestPath = testedPathParts.join(' > ')
|
||||
// console.log('elm test ',i,currentTestPath)
|
||||
let elmTest = virtualEditorElm.querySelector(currentTestPath)
|
||||
|
||||
if(!elmTest){
|
||||
break
|
||||
}
|
||||
|
||||
workingPathParts.push(pathParts[i])
|
||||
}
|
||||
|
||||
workingPath = workingPathParts.join(' > ')
|
||||
|
||||
if(workingPath){
|
||||
// Select text from virtual editor text
|
||||
selectedDivText = selectedDivText.querySelector(workingPath)
|
||||
// select text from current editor text
|
||||
newSelectedDivText = newSelectedDivText.querySelector(workingPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const oldDivText = selectedDivText.innerHTML
|
||||
const newDivText = newSelectedDivText.innerHTML
|
||||
|
||||
if(oldDivText == newDivText){ return }
|
||||
|
||||
const diff = dmp.diff_main(oldDivText, newDivText)
|
||||
const patch_list = dmp.patch_make(oldDivText, newDivText, diff)
|
||||
const patch_text = dmp.patch_toText(patch_list)
|
||||
|
||||
// save raw text for future diffs
|
||||
rawNoteText = liveEditorElm.innerHTML
|
||||
|
||||
let newPatch = {
|
||||
id: this.rawTextId,
|
||||
diff: patch_text,
|
||||
path: path,
|
||||
// testing metrics
|
||||
'old text':oldDivText,
|
||||
'new text':newDivText,
|
||||
'starting path':path,
|
||||
'working path':workingPath,
|
||||
}
|
||||
|
||||
// console.log('Sending out patch', newPatch)
|
||||
|
||||
this.$io.emit('note_diff', newPatch)
|
||||
}, 100)
|
||||
|
||||
},
|
||||
patchText(incomingPatchs){
|
||||
// console.log('incoming patches ', incomingPatchs)
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if(incomingPatchs == null){ return resolve(true) }
|
||||
if(incomingPatchs.length == 0){ return resolve(true) }
|
||||
const editorElement = document.getElementById('squire-id')
|
||||
|
||||
// let currentText = this.getText()
|
||||
let currentText = document.getElementById('squire-id').innerHTML
|
||||
|
||||
//Convert text of all new patches into patches array
|
||||
let patches = []
|
||||
// iterate over incoming patches because they apply to specific divs
|
||||
incomingPatchs.forEach(patch => {
|
||||
|
||||
if(patch.time <= this.updated){
|
||||
return
|
||||
// default to parent element, change to child if set
|
||||
let editedElement = editorElement
|
||||
if(patch.path){
|
||||
editedElement = editorElement.querySelector(patch.path)
|
||||
}
|
||||
|
||||
patches.push(...dmp.patch_fromText(patch.diff))
|
||||
if( !editedElement ){
|
||||
editedElement = editorElement
|
||||
}
|
||||
|
||||
// convert patch from text and then apply to selected element
|
||||
const patches = dmp.patch_fromText(patch.diff)
|
||||
const patchResults = dmp.patch_apply(patches, editedElement.innerHTML)
|
||||
|
||||
// console.log('Patch results')
|
||||
// console.log([patch.path, editedElement.innerHTML, patchResults[0]])
|
||||
|
||||
// patch changed directly into editor
|
||||
editedElement.innerHTML = patchResults[0]
|
||||
})
|
||||
|
||||
if(patches.length == 0){
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
var results = dmp.patch_apply(patches, currentText);
|
||||
let newText = results[0]
|
||||
|
||||
this.noteText = newText
|
||||
// this.editor.setHTML(newText)
|
||||
document.getElementById('squire-id').innerHTML = newText
|
||||
|
||||
// save editor HTML after change for future comparisons
|
||||
rawNoteText = editorElement.innerHTML
|
||||
|
||||
this.$nextTick(() => {
|
||||
return resolve(true)
|
||||
})
|
||||
})
|
||||
},
|
||||
onKeyup(event){
|
||||
|
||||
this.statusText = 'modified'
|
||||
|
||||
// Small debounce on diff generation
|
||||
clearTimeout(this.diffTextTimeout)
|
||||
this.diffTextTimeout = setTimeout(() => {
|
||||
this.diffText()
|
||||
}, 25)
|
||||
|
||||
//Save after x seconds
|
||||
clearTimeout(this.editDebounce)
|
||||
this.editDebounce = setTimeout(() => {
|
||||
@ -1030,11 +1124,11 @@
|
||||
})
|
||||
},
|
||||
destroyWebSockets(){
|
||||
// this.$io.removeListener('past_diffs')
|
||||
// this.$io.removeListener('update_user_count')
|
||||
// this.$io.removeListener('incoming_diff')
|
||||
this.$io.removeListener('past_diffs')
|
||||
this.$io.removeListener('update_user_count')
|
||||
this.$io.removeListener('incoming_diff')
|
||||
},
|
||||
setupWebSockets(){
|
||||
initWebsocketEvents(){
|
||||
|
||||
//Tell server to push this note into a room
|
||||
this.$io.emit('join_room', this.rawTextId )
|
||||
@ -1050,36 +1144,54 @@
|
||||
this.diffsApplied = diffSinceLastUpdate.length
|
||||
// console.log('Got Diffs Total -> ', diffSinceLastUpdate)
|
||||
}
|
||||
// console.log(diffSinceLastUpdate)
|
||||
this.patchText(diffSinceLastUpdate)
|
||||
.then(() => {
|
||||
this.restoreCaretPosition()
|
||||
})
|
||||
})
|
||||
|
||||
this.$io.on('incoming_diff', incomingDiff => {
|
||||
|
||||
//Save current caret position
|
||||
//Find index of child element based on past range
|
||||
const element = window.getSelection().getRangeAt(0).startContainer.parentNode
|
||||
const textLines = document.getElementById('squire-id').children
|
||||
const childIndex = [...textLines].indexOf(element)
|
||||
|
||||
this.patchText([incomingDiff])
|
||||
.then(() => {
|
||||
this.restoreCaretPosition()
|
||||
})
|
||||
})
|
||||
|
||||
},
|
||||
saveCaretPosition(event){
|
||||
|
||||
//Find index of child element based on past range
|
||||
const element = window.getSelection().getRangeAt(0).startContainer.parentNode
|
||||
|
||||
//Save range to replace cursor if someone else makes an update
|
||||
this.lastRange = this.generateSelector(element)
|
||||
this.startOffset = parseInt(event.range.startOffset) || 0
|
||||
|
||||
if(childIndex == -1){
|
||||
console.log('Cursor position lost. Div being updated was lost.')
|
||||
return
|
||||
}
|
||||
},
|
||||
restoreCaretPosition(){
|
||||
return new Promise((resolve, reject) => {
|
||||
// This code is intended to restore caret position to previous location
|
||||
// when a third party updates the note.
|
||||
|
||||
if(!this.lastRange){ return resolve(true) }
|
||||
|
||||
const editorElement = document.getElementById('squire-id')
|
||||
const lastElement = editorElement.querySelector(this.lastRange)
|
||||
|
||||
if( !lastElement ){ return resolve(true) }
|
||||
|
||||
//Reset caret position
|
||||
//Find child index of old range and create a new one
|
||||
let allChildren = document.getElementById('squire-id').children
|
||||
const newLine = allChildren[childIndex].firstChild
|
||||
let range = document.createRange()
|
||||
range.setStart(newLine, this.startOffset)
|
||||
range.setEnd(newLine, this.startOffset)
|
||||
this.editor.setSelection(range)
|
||||
})
|
||||
})
|
||||
range.setStart(lastElement.firstChild, this.startOffset)
|
||||
range.setEnd(lastElement.firstChild, this.startOffset)
|
||||
|
||||
// Set range in editor element
|
||||
this.editor.setSelection(range)
|
||||
|
||||
return resolve(true)
|
||||
})
|
||||
},
|
||||
titleResize(){
|
||||
//Resize the title field
|
||||
@ -1102,6 +1214,11 @@
|
||||
z-index: 1019;
|
||||
text-align: right;
|
||||
}
|
||||
.status-menu span + span {
|
||||
border-left: 1px solid #ccc;
|
||||
margin-left: 4px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.font-color-bar {
|
||||
/*width: calc(100% - 8px);*/
|
||||
|
@ -37,10 +37,6 @@
|
||||
|
||||
<paste-button />
|
||||
|
||||
<span class="ui grey text text-fix">
|
||||
Active Sessions {{ $store.getters.getActiveSessions }}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="eight wide column" v-if="showClear">
|
||||
@ -571,7 +567,7 @@
|
||||
// @TODO Don't even trigger this if the note wasn't changed
|
||||
updateSingleNote(noteId, focuseAndAnimate = true){
|
||||
|
||||
console.log('updating single note', noteId)
|
||||
// console.log('updating single note', noteId)
|
||||
|
||||
noteId = parseInt(noteId)
|
||||
|
||||
|
@ -114,29 +114,12 @@ io.on('connection', function(socket){
|
||||
|
||||
//Emit all sorted diffs to user
|
||||
socket.emit('past_diffs', noteDiffs[rawTextId])
|
||||
} else {
|
||||
socket.emit('past_diffs', null)
|
||||
}
|
||||
|
||||
const usersInRoom = io.sockets.adapter.rooms[rawTextId]
|
||||
if(usersInRoom){
|
||||
//Update users in room count
|
||||
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
|
||||
|
||||
//Debugging text - prints out notes in limbo
|
||||
let noteDiffKeys = Object.keys(noteDiffs)
|
||||
let totalDiffs = 0
|
||||
noteDiffKeys.forEach(diffSetKey => {
|
||||
if(noteDiffs[diffSetKey]){
|
||||
totalDiffs += noteDiffs[diffSetKey].length
|
||||
}
|
||||
})
|
||||
//Debugging Text
|
||||
if(noteDiffKeys.length > 0){
|
||||
console.log('Total notes in limbo -> ', noteDiffKeys.length)
|
||||
console.log('Total Diffs for all notes -> ', totalDiffs)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
@ -162,31 +145,13 @@ io.on('connection', function(socket){
|
||||
|
||||
noteDiffs[noteId].push(data)
|
||||
|
||||
//Remove duplicate diffs if they exist
|
||||
for (var i = noteDiffs[noteId].length - 1; i >= 0; i--) {
|
||||
|
||||
let pastDiff = noteDiffs[noteId][i]
|
||||
|
||||
for (var j = noteDiffs[noteId].length - 1; j >= 0; j--) {
|
||||
let currentDiff = noteDiffs[noteId][j]
|
||||
|
||||
if(i == j){
|
||||
continue
|
||||
}
|
||||
|
||||
if(currentDiff.diff == pastDiff.diff || currentDiff.time == pastDiff.time){
|
||||
console.log('Removing Duplicate')
|
||||
noteDiffs[noteId].splice(i,1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Each user joins a room when they open the app.
|
||||
// Go over each user in this note-room
|
||||
io.in(noteId).clients((error, clients) => {
|
||||
if (error) throw error;
|
||||
|
||||
//Go through each client in note room and send them the diff
|
||||
//Go through each client in note-room and send them the diff
|
||||
clients.forEach(socketId => {
|
||||
// only send off diff if user
|
||||
if(socketId != socket.id){
|
||||
io.to(socketId).emit('incoming_diff', data)
|
||||
}
|
||||
@ -213,7 +178,6 @@ io.on('connection', function(socket){
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo)
|
||||
|
||||
if(noteDiffs[checkpoint.rawTextId].length == 0){
|
||||
|
Loading…
Reference in New Issue
Block a user