SolidScribe/client/src/components/NoteInputPanel.vue
Max G e7d1cc7bc9 * Added theme colors to form fields
* Added some basic table styles for inserting some shitty tables
* Made popup notification styles look better and work better on mobile
* Quick note now opens a note and not some weird page
* Menu collapses when page is small, behaves like mobile menu
* Added terms and conditions to help and login forms
* Added password change functionality
* Better styles for shared page
* Added some tests for changing password
2020-07-14 05:31:02 +00:00

1435 lines
39 KiB
Vue

<template>
<!-- change class to .master-note-edit to have it popup on the screen -->
<div
id="InputNotes"
class="master-note-edit full-focus position-0"
@keyup.esc="closeButtonAction()"
>
<!-- Giant Edit Note Menu -->
<div class="edit-menu" :class="{ 'slide-out-top':(sizeDown == true) }">
<!-- edit spacer is disabled, it is helpful if menu gets bigger. It adds a left margin, starting the icons at the edge of the note -->
<div class="edit-spacer"></div>
<div class="menu-top-half">
<div class="edit-button" v-on:click="closeButtonAction()" data-tooltip="Close" data-position="bottom center" data-inverted>
<i class="close icon"></i>
</div>
<div class="edit-divide"></div>
<div class="edit-button" v-on:click="toggleList('ul')" data-tooltip="Task List" data-position="bottom center" data-inverted :class="{'edit-active':activeToDo}">
<i class="tasks icon"></i>
</div>
<div class="edit-button" v-on:click="toggleList('ol')" data-tooltip="Numbered List" data-position="bottom center" data-inverted :class="{'edit-active':activeList}">
<i class="list ol icon"></i>
</div>
<div class="edit-divide"></div>
<div class="edit-button" v-on:click="colorpicker = true" data-tooltip="Text Color" data-position="bottom center" data-inverted :style="{'color':activeColor}">
<i class="font icon"></i>
</div>
<div class="edit-button" v-on:click="toggleBold()" data-tooltip="Bold" data-position="bottom center" data-inverted :class="{'edit-active':activeBold}">
<i class="bold icon"></i>
</div>
<div class="edit-button" v-on:click="toggleItalic()" data-tooltip="Italic" data-position="bottom center" data-inverted :class="{'edit-active':activeItalics}">
<i class="italic icon"></i>
</div>
<div class="edit-button" v-on:click="toggleUnderline()" data-tooltip="Underline" data-position="bottom center" data-inverted :class="{'edit-active':activeUnderline}">
<i class="underline icon"></i>
</div>
<div class="edit-button" v-on:click="modifyFont('1.4em')" data-tooltip="Title" data-position="bottom center" data-inverted :class="{'edit-active':activeTitle}">
<i class="text height icon"></i>
</div>
<div class="edit-divide"></div>
<div class="edit-button" v-on:click="editor.increaseQuoteLevel()" data-tooltip="Indent" data-position="bottom center" data-inverted>
<i class="indent icon"></i>
</div>
<div class="edit-button" v-on:click="editor.decreaseQuoteLevel()" data-tooltip="Outdent" data-position="bottom center" data-inverted>
<i class="outdent icon"></i>
</div>
</div>
<div class="menu-bottom-half">
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/table`)" data-tooltip="Insert Table" data-position="bottom center" data-inverted>
<i class="border all icon"></i>
</div>
<div class="edit-button" v-on:click="insertDivide()" data-tooltip="Insert Divide" data-position="bottom center" data-inverted>
<i class="grip lines icon"></i>
</div>
<div class="edit-button" v-on:click="removeFormatting()" data-tooltip="Remove Formatting" data-position="bottom center" data-inverted>
<i class="remove format icon"></i>
</div>
<div class="edit-button" v-on:click="undoCustom()" data-tooltip="Undo" data-position="bottom center" data-inverted>
<i class="undo icon"></i>
</div>
<div class="edit-divide"></div>
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/colors`)" data-tooltip="Note Color" data-position="bottom center" data-inverted :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<i class="paint brush icon"></i>
</div>
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/tags`)" data-tooltip="Tags" data-position="bottom center" data-inverted>
<i class="tags icon"></i>
</div>
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/images`)" data-tooltip="Images" data-position="bottom center" data-inverted>
<i class="image icon"></i>
</div>
<file-upload-button
data-tooltip="Upload File" data-position="bottom center" data-inverted
class="edit-button"
:noteId="noteid" />
<div class="edit-divide"></div>
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/options`)" data-tooltip="More Options" data-position="bottom center" data-inverted>
<i class="ellipsis horizontal icon"></i>
</div>
<div class="edit-divide"></div>
<!--
<div class="edit-button" v-on:click="onToggleArchived()" :data-tooltip="archived == 1?'Move to main list':'Move to Archive'" data-position="bottom center" data-inverted>
<span v-if="archived == 1"><i class="green archive icon"></i></span>
<span v-if="archived != 1"><i class="archive icon"></i></span>
</div>
<div class="edit-button" v-on:click="onTogglePinned" :data-tooltip="pinned == 1?'Un-pin from top':'Pin to top'" data-position="bottom center" data-inverted>
<span v-if="pinned == 1"><i class="green pin icon"></i></span>
<span v-if="pinned != 1"><i class="pin icon"></i></span>
</div> -->
<div class="edit-button" v-if="usersOnNote > 1">
<i class="green eye icon"></i> {{ usersOnNote }}
</div>
<!--
<div class="edit-button" v-on:click="simulateTyping()">
<i class="purple bolt icon"></i>
</div> -->
<div class="edit-button" v-on:click=" hash=0; save() ">
<i class="icons">
<i class="grey save outline icon"></i>
<i v-if="statusText == 'saved'" class="green small bottom left corner check icon"></i>
<i v-if="statusText == 'saving'" class="small purple bottom left corner double angle up icon"></i>
</i>
</div>
<div class="edit-button" v-if="diffsApplied > 0">
+{{ diffsApplied }}
</div>
</div>
</div>
<div class="bottom-edit-menu"></div>
<div class="input-container-wrapper" :class="{ 'side-menu-open':sideMenuOpen, 'size-down':(sizeDown == true),}">
<!-- Squire box grows -->
<div id="text-box-container" class="note-wrapper" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<!-- Loading indicator -->
<transition name="fade">
<div v-if="loading || forceShowLoading" class="loading-note" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<div class="loading-text">
<loading-icon :message="loadingMessage" />
</div>
</div>
</transition>
<!-- Title input area -->
<textarea
ref="titleTextarea"
v-on:keyup="titleResize"
v-on:keydown="titleResize"
@keydown.enter.exact.prevent="editor.focus(); editor.moveCursorToEnd()"
rows="1"
:style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}"
v-on:blur="save" type="text" v-model="noteTitle" placeholder="Title" class="stealth-input">
</textarea>
<!-- Squire Box -->
<div id="squire-id" class="squire-box" ref="squirebox" placeholder="Type Note Here"></div>
</div>
</div>
<!-- little tags on the side -->
<div class="note-mini-tag-area" :class="{ 'size-down':sizeDown }">
<span v-for="tag in allTags" class="subtle-tag active-mini-tag" v-if="isTagOnNote(tag.id)" v-on:click="removeTag(tag.id)">
<i class="tag icon"></i>
{{ tag.text }}
</span>
<span v-else class="subtle-tag" v-on:click="addTag(tag.text)">
<i class="plus icon"></i>
{{ tag.text }}
</span>
<span class="subtle-tag" v-on:click="$router.push(`/notes/open/${noteid}/menu/tags`)">
<i class="plus icon"></i><i class="green tags icon"></i>Add Tag
</span>
</div>
<!-- color picker -->
<color-tooltip
v-if="colorpicker"
v-on:color="color => modifyColor(color)"
v-on:close="colorpicker = false"
/>
<!-- Side slide menus for colors, tags, images and other options -->
<side-slide-menu v-if="colors" v-on:close="colors = false" name="colors">
<color-picker
@changeColor="onChangeColor"
@close="colors = false; $router.go(-1)"
:style-object="styleObject"
/>
</side-slide-menu>
<side-slide-menu v-if="tags" v-on:close="tags = false; fetchNoteTags()" name="tags" :style-object="styleObject">
<div class="ui basic segment">
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/>
</div>
</side-slide-menu>
<side-slide-menu v-if="images" v-on:close="images = false" name="images" :style-object="styleObject">
<div class="ui basic segment">
<simple-attachment-note
:note-id="noteid"
:squire-editor="editor">
</simple-attachment-note>
</div>
</side-slide-menu>
<side-slide-menu v-if="options" v-on:close="options = false" name="note-options" :style-object="styleObject">
<div class="ui basic padded segment">
<div class="ui grid">
<div class="sixteen wide column">
<h2>Additional Note Options</h2>
</div>
<div class="sixteen wide column">
<div class="ui labeled icon fluid basic button" v-on:click="sortList">
<i class="sort amount up icon"></i>
Sort List
</div>
</div>
<div class="eight wide column">
<div class="ui labeled icon fluid basic button" v-on:click="deleteCompletedListItems">
<i class="trash icon"></i>
Delete Checked
</div>
</div>
<div class="eight wide column">
<div class="ui labeled icon fluid basic button" v-on:click="uncheckAllListItems">
<i class="list ul icon"></i>
Uncheck All
</div>
</div>
<div class="eight wide column">
<div class="ui labeled icon fluid basic button" v-on:click="calculateMath" data-tooltip="Calculates algebra before '='">
<i class="calculator icon"></i>
Simple Math
</div>
</div>
<div class="eight wide column">
<!-- data-tooltip="Files on note" -->
<div v-on:click="openEditAttachment" class="ui labeled icon fluid basic button">
<i class="folder icon"></i>
Note Files
{{ attachmentCount }}
</div>
</div>
<div class="sixteen wide column" v-if="rawTextId > 0">
<h2>Share Note</h2>
<share-note-component
:note-id="noteid"
:raw-text-id="rawTextId"
:share-username="shareUsername"
/>
</div>
</div>
</div>
</side-slide-menu>
<!-- create table option -->
<side-slide-menu v-if="table" v-on:close="table = false; fetchNoteTags()" name="table" :style-object="styleObject">
<div class="ui basic segment">
<h2>Insert Table</h2>
<div class="table-tic-table">
<div v-for="i in 10">
<div v-for="j in 10" class="tabletic" v-on:click="insertTable(i,j)">
</div>
</div>
</div>
</div>
</side-slide-menu>
<!-- Show side shades if user is on desktop only -->
<div class="full-focus-shade shade1"
:class="{ 'slide-out-left':sizeDown }"
v-on:click="closeButtonAction()"></div>
<div class="full-focus-shade shade2"
:class="{ 'slide-out-right':sizeDown }"
v-on:click="closeButtonAction()"></div>
</div>
</template>
<script>
import axios from 'axios'
// const crypto = require('crypto')
const DiffMatchPatch = require('../../../server/helpers/DiffMatchPatch')
const dmp = new DiffMatchPatch.diff_match_patch()
import SquireButtonFunctions from '@/mixins/SquireButtonFunctions.js'
export default {
name: 'InputNotes',
props: [ 'noteid', 'position', 'openMenu', 'urlData' ],
components:{
'note-tag-edit': () => import('@/components/NoteTagEdit.vue'),
'color-picker': () => import('@/components/ColorPicker.vue'),
'file-upload-button': () => import('@/components/FileUploadButton.vue'),
// 'delete-button': () => import('@/components/NoteDeleteButtonComponent.vue'),
'side-slide-menu': () => import('@/components/SideSlideMenuComponent.vue'),
'simple-attachment-note': () => import('@/components/SimpleAttachmentNoteComponent.vue'),
'share-note-component': () => import('@/components/ShareNoteComponent.vue'),
'color-tooltip':require('@/components/TextColorTooltipComponent.vue').default,
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default,
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
},
mixins:[ SquireButtonFunctions ],
data(){
return {
loading: true,
forceShowLoading: true,
loadingMessage: 'Loading Note',
currentNoteId: 0,
modified: false,
noteText: '',
noteTitle: '',
rawTextId: 0,
created: '',
updated: '',
shareUsername: null,
// diffNoteText: '',
statusText: 'saved',
lastNoteHash: null,
saveDebounce: null, //Prevent save from being called numerous times quickly
updated: 'Never',
editDebounce: null,
emitChangeDebounce: null,
keyPressesCounter: 0, //Determen keys pressed between saves
pinned: 0,
archived: 0,
attachmentCount: 0,
styleObject: { 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, //Style object. Determines colors and badges
sizeDown: false, //Used to animate close state
//Settings vars
lastVisibilityState: null,
//All the squire settings
editor: null,
usersOnNote: 0,
sideMenuOpen: false,
tags: false,
colors: false,
images: false,
options: false,
colorpicker: false,
table: false,
//Diff text/sync text variables
diffTextTimeout: null,
diffsApplied: null,
//Used to restore caret position
lastRange: null,
startOffset: 0,
//Tag Display
allTags: [],
noteTags: [],
}
},
watch: {
urlData(newVal, oldVal){
//Handle changes in URL to
if(newVal.id == undefined || newVal.id != this.noteid){
// this.closeButtonAction()
}
//Reset all note menus on URL change
this.sideMenuOpen = false
this.colors = false
this.tags = false
this.options = false
this.images = false
this.table = false
//If a menu value is set, open it
if(newVal.openMenu){
//Only modify menu boolean if its defined
if(typeof this[newVal.openMenu] == 'boolean'){
this.sideMenuOpen = true
this[newVal.openMenu] = true
}
}
}
},
beforeMount(){
this.$bus.$on('new_file_upload', ({noteId, imageCode}) => {
if(this.noteid == noteId && this.editor){
this.editor.moveCursorToEnd()
this.editor.insertHTML(imageCode)
this.save()
}
})
},
beforeDestroy(){
this.$io.emit('leave_room', this.rawTextId)
this.$bus.$off('new_file_upload')
this.destroyWebSockets()
document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
//Obliterate squire instance
this.editor.destroy()
this.close()
},
mounted: function() {
//Show loading for a minimum time
setTimeout(()=>{
this.forceShowLoading = false
}, 500)
// document.addEventListener('visibilitychange', this.checkForUpdatedNote)
//Init squire as early as possible
this.editor = new Squire( this.$refs.squirebox, {blockTag: 'p' })
this.$nextTick(() => {
//Setup Squire first chance we get
this.loadNote(this.noteid)
})
},
methods: {
testModify(){
const text = document.getElementById('squire-id').children[0].innerHTML
const prepended = 'Hello-> ' + text
document.getElementById('squire-id').children[0].innerHTML = prepended
const text1 = document.getElementById('squire-id').children[2].innerHTML
const prepended1 = 'Hey-> ' + text1
document.getElementById('squire-id').children[2].innerHTML = prepended1
},
simulateTyping(index = 0){
const words = ['lets','see','how','big','of','a','list','we','can','make','and','if','we','can','simulate','multiple','users','typing','its','probably','going','to','be','a','shitty','shit','show','but','whatever','~','You all','ever','seen','a','full','size','zebra','eat','a','big','fat','mound','of','grass','let','me','tell','you','man,','those','freaking','things','can','chew.','I','mean','like','really','chew.','Not','just','a','little,','but','a','lot.','~','Do not','believe','me.','Shoot','on','down','to','your','local','zoo','with','a','fat','mound','of','grass.','~','In','other','news,','I','really','enjoy','testing','things.','I am','just','going','to','keep','on','typing','until','its','so','annoying','that','you','give','up','on','trying','to','type','other','things.','~','Other','people','do not','get','to','type!','Only','me!','Fuck','off','other','users.','Its','all','about','me','up','in','here.','And','~','Zebra','Grass.',
]
const nextWord = words[index % (words.length)] + ' '
let letterObjects = []
let totalTime = 0
nextWord.split('').forEach(letter => {
const letterTime = Math.floor(Math.random() * 80) + 20
totalTime += letterTime
setTimeout(() => {
const newText = this.getText().slice(0,-8)
if(letter == ' '){
letter = ' <br></p>'
}
if(letter == '~'){
letter = '<br></p><p><br></p><p><br></p>'
}
const addedLetter = newText + letter
this.setText(addedLetter)
this.onKeyup()
}, totalTime)
})
setTimeout(() => {
if(index < words.length-1){
const more = index + 1
this.simulateTyping(more)
}
}, totalTime + 40)
},
removeTag(tagId){
this.allTags = []
let entryId = 0
//Find fucking note tag for removal
this.noteTags.forEach(noteTag => {
if(noteTag['tagId'] == tagId){
entryId = noteTag['entryId']
}
})
let postData = {
'tagId':entryId,
'noteId':this.noteid
}
axios.post('/api/tag/removefromnote', postData)
.then(response => {
this.fetchNoteTags()
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Remove Tag') })
},
addTag(tagText){
this.allTags = []
let postData = {
'tagText':tagText,
'noteId':this.noteid
}
axios.post('/api/tag/addtonote', postData)
.then(response => {
this.fetchNoteTags()
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Add Tag') })
},
fetchNoteTags(){
axios.post('/api/tag/get', {'noteId': this.noteid})
.then(({data}) => {
this.allTags = data.allTags
this.noteTags = data.noteTagIds
//Stick used tags at top.
if(this.noteTags.length > 0){
let frontTags = []
for (var i = this.allTags.length - 1; i >= 0; i--) {
this.noteTags.forEach(noteTag => {
if(this.allTags[i]['id'] == noteTag['tagId']){
frontTags.push(this.allTags[i])
this.allTags.splice(i,1)
}
})
}
this.allTags.unshift(...frontTags)
}
})
},
isTagOnNote(id){
for (let i = 0; i < this.noteTags.length; i++) {
const current = this.noteTags[i]
if(current && current['tagId'] == id){
return true
}
}
return false
},
initSquire(){
//Set up squire and load note text
this.setText(this.noteText)
//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()
this.fetchNoteTags() //Don't load tags on mobile
}
//Set up websockets after squire is set up
setTimeout(() => {
this.setupWebSockets()
}, 50)
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
})
//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))
//Click Event - Open links when clicked in editor or toggle checks
this.editor.addEventListener('click', e => {
//Link clicked in editor - open link
if(e.target.nodeName == 'A' && e.target.href){
window.open(e.target.href)
}
//List Item clicked in editor - toggle link state
if(e.target.nodeName == 'LI'){
let el = e.target
//If the offset is triggered with a negative offset, it means the before element was clicked
if(e.offsetX < -5){
//Will hide keyboard if clicked on mobile
if(this.$store.getters.getIsUserOnMobile){
this.editor.blur()
}
//Area before element was clicked, they clicked the checkbox
if (el.className === 'active'){
el.className = 'inactive';
} else {
el.className = 'active';
}
}
}
})
this.editor.addEventListener('keydown', event => {
//Tab to increase quote level, tab + shigt to decrease quote level
const keyCode = event.key
if(keyCode == 'Tab'){
if(event.shiftKey){
this.editor.decreaseQuoteLevel()
} else {
this.editor.increaseQuoteLevel()
}
event.preventDefault()
return false
}
//Save on pressing CTRL/CMD + S
if(keyCode == 's' && (event.ctrlKey || event.metaKey) ){
this.$bus.$emit('notification', 'Note Saved')
this.save()
event.preventDefault()
return false
}
//Prevent new list items from having
this.$nextTick( () => {
//Wait a moment to get item under cursor
let selection = this.editor.getSelection()
let container = selection.commonAncestorContainer
//If user hit enter on a list, make sure the next list item isn't active
if(container.nodeName == 'LI' && event.keyCode == 13 && container.classList){
container.classList.remove('active')
}
})
})
//Bind event handlers
this.editor.addEventListener('keyup', event => {
this.onKeyup(event)
})
//Show and hide additional toolbars
this.editor.addEventListener('focus', e => {
})
this.editor.addEventListener('blur', e => {
})
},
openEditAttachment(){
this.$router.push('/attachments/note/'+this.currentNoteId)
},
onTogglePinned(){
if(this.pinned == 0){
this.pinned = 1
} else {
this.pinned = 0;
}
//Update last note hash, this will tell note to save next update
this.lastNoteHash = 0
this.save()
},
onToggleArchived(){
if(this.archived == 0){
this.archived = 1
} else {
this.archived = 0;
}
//Update last note hash, this will tell note to save next update
this.lastNoteHash = 0
this.save()
},
onChangeColor(newStyleObject){
//Set new style object for note, page will use some styles, styles will be saved to database
this.styleObject = newStyleObject
this.lastNoteHash = 0 //Update hash to force note update on next save
this.save()
},
loadNote(noteId){
//Generate a random loading message
let mod = ['Gently','Calmly','Lovingly','Quickly','','','','','','','','','','','','','']
let doing = ['Loading','Loading','Getting','Fetching','Grabbing','Sequencing','Organizing','Untangling','Processing','Refining','Extracting','Fusing','Pruning','Expanding','Enlarging','Transfiguring','Quantizing','Ingratiating','Lumping']
let thing = ['Note','Note','Note','Note','Data','Text','Document','Algorithm','Buffer','Client','Download','File','Frame','Graphics','Hardware','HTML','Interface','Logic','Mainframe','Memory','Media','Nodes','Network','Chaos']
let p1 = mod[Math.floor(Math.random() * mod.length)]
let p2 = doing[Math.floor(Math.random() * doing.length)]
let p3 = thing[Math.floor(Math.random() * thing.length)]
this.loadingMessage = `${p1} ${p2} ${p3}`
//Component is activated with NoteId in place, lookup text with associated ID
if(this.$store.getters.getLoggedIn){
axios.post('/api/note/get', { 'noteId': this.noteid })
.then(response => {
//Block notes you don't have access to from opening
if(response.data === false){
this.$bus.$emit('notification', 'Error opening Note')
this.close()
return
}
//Set up local data
this.currentNoteId = this.noteid
this.rawTextId = response.data.rawTextId
this.shareUsername = response.data.shareUsername
this.created = response.data.created
this.updated = response.data.updated
this.noteTitle = ''
if(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
//Set up note colors
if(response.data.color){
this.styleObject = JSON.parse(response.data.color)
}
if(response.data.pinned != null){
this.pinned = response.data.pinned
}
this.archived = response.data.archived
this.attachmentCount = response.data.attachment_count
this.loading = false
this.$nextTick(() => {
//Adjust note title size after load
this.titleResize()
this.initSquire()
})
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Open Note') })
} else {
console.log('Could not fetch note')
}
},
//Called on squire event for keyup
diffText(event){
//Diff the changed lines only
let oldText = this.noteText
// let newText = this.getText()
let newText = document.getElementById('squire-id').innerHTML
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);
if(patch_text == ''){ return }
//Save computed diff text
this.noteText = newText
let newPatch = {
id: this.rawTextId,
diff: patch_text,
}
this.$io.emit('note_diff', newPatch)
},
patchText(incomingPatchs){
return new Promise((resolve, reject) => {
if(incomingPatchs == null){ return resolve(true) }
if(incomingPatchs.length == 0){ return resolve(true) }
// let currentText = this.getText()
let currentText = document.getElementById('squire-id').innerHTML
//Convert text of all new patches into patches array
let patches = []
incomingPatchs.forEach(patch => {
if(patch.time <= this.updated){
return
}
patches.push(...dmp.patch_fromText(patch.diff))
})
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
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(() => {
this.save()
}, 30 * 1000)
//Save after x keystrokes
this.keyPressesCounter = (this.keyPressesCounter + 1)
if(this.keyPressesCounter > 125){
this.keyPressesCounter = 0
this.save()
}
},
save(force = false){
return new Promise((resolve, reject) => {
//Clear other debounced events to prevent double calling of save
// clearTimeout(this.editDebounce)
if(this.statusText == 'saving'){
return resolve(true)
}
//Don't save note if its hash doesn't change
const currentNoteText = this.getText()
const currentHash = this.hashString( currentNoteText )
if( this.lastNoteHash == currentHash){
this.statusText = 'saved'
return resolve(true)
}
//If user accidentally clears note, it won't delete it
if(currentNoteText == ''){
return resolve(true)
}
//tell websockets to truncate history at this save
this.$io.emit('truncate_diffs_at_save', {'rawTextId':this.rawTextId, 'hash':currentHash })
const postData = {
'noteId': this.currentNoteId,
'title': this.noteTitle,
'text': currentNoteText,
'color': JSON.stringify(this.styleObject), //Save little json color object
'pinned': this.pinned,
'archived': this.archived,
'hash': currentHash,
}
this.statusText = 'saving'
axios.post('/api/note/update', postData).then( response => {
this.statusText = 'saved'
this.updated = +new Date
this.modified = true
this.diffsApplied = 0
//Update last saved note hash
this.lastNoteHash = currentHash
return resolve(true)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Save Note') })
})
},
checkForUpdatedNote(){
//Ignore visibility changes, handle this with socket IO
//Just keep it always up to date if user is on note
return
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
// console.log('Checking for note updates after visibility change.')
const postData = {
noteId:this.currentNoteId,
text:this.getText(),
updated: this.updated
}
console.log('Focus regained with note open.')
console.log('Attempting to fix diff text. fix this. Search spleen')
return
axios.post('/api/note/difftext', postData)
.then( ({data}) => {
//Don't do anything if nothing has changed
if(data == ''){ return }
if(data.diffs > 0){
//Update text and last updated time for note
this.setText(data.updatedText)
this.updated = data.updated
}
})
.catch(error => { this.$bus.$emit('notification', 'Failed to get diff') })
}
//Track visibility state
this.lastVisibilityState = document.visibilityState
},
hashString(inText){
let text = this.noteTitle + inText
let hash = 0;
if (text == null || text.length == 0) {
return hash;
}
for (let i = 0; i < text.length; i++) {
let char = text.charCodeAt(i);
hash = ((hash<<5)-hash)+char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
},
closeButtonAction(){
this.sizeDown = true
//This timeout allows animation to play before closing
setTimeout(() => {
this.$router.push('/notes')
}, 300)
},
close(){
// force = true
// console.log(`Close Note ${this.noteid} -> force: ${force}, modified: ${this.modified}`)
//Skip everything if foce close is true. Note will just die.
if(this.currentNoteId == 0){ return }
this.loadingMessage = 'Saving...'
this.loading = true
this.save().then( result => {
//If note was modified, trigger reindex on close
if(this.modified){
axios.post('/api/note/reindex')
}
this.$bus.$emit('close_active_note', {
noteId: this.noteid, modified: this.modified
})
return
})
},
destroyWebSockets(){
this.$io.removeListener('past_diffs')
this.$io.removeListener('update_user_count')
this.$io.removeListener('incoming_diff')
},
setupWebSockets(){
//Tell server to push this note into a room
this.$io.emit('join_room', this.rawTextId )
this.$io.on('update_user_count', userCount => {
this.usersOnNote = userCount
})
//Apply all diffs since last save
this.$io.on('past_diffs', diffSinceLastUpdate => {
if(diffSinceLastUpdate != null){
this.diffsApplied = diffSinceLastUpdate.length
// console.log('Got Diffs Total -> ', diffSinceLastUpdate)
}
this.patchText(diffSinceLastUpdate)
})
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(() => {
if(childIndex == -1){
console.log('Cursor position lost. Div being updated was lost.')
return
}
//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)
})
})
},
titleResize(){
//Resize the title field
let element = this.$refs.titleTextarea
let padding = 0
element.style.height = 'auto'
element.style.height = (element.scrollHeight + padding) +'px'
},
}
}
</script>
<style type="text/css" scoped>
.full-focus-shade {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--menu-accent);
z-index: 999;
cursor: pointer;
opacity: 0.8;
}
.shade1 {
left: 50%;
}
.shade2 {
right: 50%;
}
/* squire styles */
.input-container-wrapper {
position: fixed;
top: 0;
bottom: 0;
left: 15%;
right: 15%;
z-index: 1005;
overflow-x: scroll;
scrollbar-width: none;
scrollbar-color: transparent transparent;
}
.note-wrapper {
background-color: var(--small_element_bg_color);
border: 1px solid var(--menu-accent);
margin: 45px 0 45px 0;
position: relative;
}
.note-mini-tag-area {
position: fixed;
width: 120px;
left: calc(15% - 125px);
top: 46px;
bottom: 0;
height: calc(100vh - 55px);
z-index: 1000;
overflow-y: scroll;
scrollbar-width: none;
scrollbar-color: transparent transparent;
}
.note-mini-tag-area {
scrollbar-width: auto;
scrollbar-color: inherit inherit;
}
.subtle-tag {
display: inline-block;
width: 100%;
padding: 1px 1px 1px 5px;
margin: 0 0 0;
border: 1px solid transparent;
border-right: none;
border-radius: 3px;
background-color: transparent;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color ease 0.3s, background ease 0.3s;
font-size: 11px;
cursor: pointer;
opacity: 0;
text-transform:capitalize;
}
.note-mini-tag-area:hover .subtle-tag {
opacity: 1;
}
.note-mini-tag-area:hover .active-mini-tag {
background-color: var(--main-accent);
color: white;
}
.note-mini-tag-area:hover .subtle-tag:not(.active-mini-tag) {
border-right: none;
color: var(--text_color);
background-color: var(--body_bg_color);
opacity: 1;
}
.active-mini-tag {
opacity: 0.7;
background-color: var(--small_element_bg_color);
color: var(--text_color)
}
@keyframes blinker {
50% {
opacity: 0.2;
}
}
/*
Edit Menu Styles START
*/
.edit-menu {
position: fixed;
top: 0;
left: 0;
right: 0;
width: 100%;
display: block;
background-color: var(--small_element_bg_color);
z-index: 1019;
padding: 3px 5px;
border: none;
border-bottom: 1px solid var(--menu-accent);
text-align: center;
}
.menu-top-half, .menu-bottom-half {
display: inline-block;
}
/* .edit-spacer {
width: calc(15% - 10px);
display: inline-block;
height: 10px;
opacity: 0;
}*/
.edit-button {
background-color: var(--small_element_bg_color);
color: var(--menu-text);
display: inline-block;
font-size: 1.4em;
padding: 4px 1px 5px 4px;
border-radius: 3px;
cursor: pointer;
}
.edit-button:hover {
background-color: var(--menu-accent);
}
.edit-active {
background-color: var(--main-accent);
color: white;
}
.edit-divide {
display: inline-block;
background-color: var(--menu-accent);
height: 15px;
width: 1px;
margin: 0 1px;
padding: 0;
}
@media only screen and (max-width: 740px) {
.edit-spacer {
display: none;
}
.edit-button {
font-size: 1.2em;
}
}
/*
Edit Menu Styles END
*/
.stealth-input {
width: 100%;
padding: 10px 15px 5px;
background-color: rgba(255,255,255,0.1);
border: none;
font-size: 1.7em;
color: var(--text_color);
resize: none;
overflow: hidden;
margin: 0;
outline: none;
}
/*Settings manager styles */
.all-settings {
background: #221f2b;
z-index: 99;
flex-grow: 0;
}
/*End Settings manager styles */
/* container styles change based on mobile and number of open screens */
.master-note-edit {
position: fixed;
bottom: 0;
height: 100vh;
z-index: 1001;
overflow-y: scroll;
overflow-x: hidden;
scrollbar-width: none;
scrollbar-color: transparent transparent;
}
.loading-note {
position: absolute;
top: 0;
width: 100%;
height: 100%;
min-height: 300px;
/*background: var(--small_element_bg_color);*/
/*opacity: 0.;*/
z-index: 1;
}
.loading-text {
margin: 0;
position: absolute;
top: 200px;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
}
/* One note open, in the middle of the screen */
.master-note-edit.position-0 {
left: 50%;
right: 0;
}
.master-note-edit.full-focus {
left: 15%;
right: 15%;
}
.side-menu-open {
left: calc(50% + 10px) !important;
right: calc(0% + 10px) !important;
}
@media only screen and (max-width: 740px) {
.input-container-wrapper {
left: 0;
right: 0;
top: 35px;
bottom: 40px;
background-color: var(--small_element_bg_color);
}
.note-wrapper {
margin: 0;
border: none;
}
.shade1, .shade2 {
right: 150%;
}
/*menu overwrites */
.bottom-edit-menu {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
display: block;
background-color: var(--small_element_bg_color);
z-index: 1012;
border: none;
border-top: 1px solid var(--menu-accent);
min-height: 40px;
}
.edit-menu {
text-align: center;
}
.menu-bottom-half {
z-index: 1005;
position: fixed;
bottom: 4px;
left: 0;
right: 0;
text-align: center;
}
}
/* Two Notes Open, each takes up 50% of the space */
.master-note-edit.position-1 {
left: 50%;
right: 0%;
}
.master-note-edit.position-2 {
left: 0%;
right: 50%;
}
.master-note-edit.position-3 {
display: inline-block;
position: inherit;
width: 100%;
min-height: 200px;
height: auto;
box-shadow: none;
}
/* animations START */
.slide-out-top {
animation: slide-out-top 0.5s ease;
}
@keyframes slide-out-top {
0% {
top: 0;
}
100% {
top: -100px;
}
}
.size-down {
animation: size-down 0.5s ease;
}
@keyframes size-down {
0% {
top: 0;
}
100% {
top: 150vh;
}
}
.slide-out-left {
animation: slide-out-left 0.5s ease;
}
@keyframes slide-out-left {
0% {
left: 85%;
}
100% {
left: 150%;
}
}
.slide-out-right {
animation: slide-out-right 0.5s ease;
}
@keyframes slide-out-right {
0% {
right: 85%;
}
100% {
right: 150%;
}
}
/* Fade out transition animation */
.fade-enter {
/*opacity: 0;*/
}
.fade-enter-active {
/*transition: opacity 0.7s;*/
}
.fade-leave {
/* opacity: 0; */
}
.fade-leave-active {
transition: opacity 0.7s;
opacity: 0;
}
/* animations END */
</style>