SolidScribe/client/src/components/NoteInputPanel.vue

1125 lines
30 KiB
Vue
Raw Normal View History

<template>
<!-- change class to .master-note-edit to have it popup on the screen -->
<div
id="InputNotes"
class="master-note-edit"
@keyup.esc="close"
2020-02-01 14:21:22 -08:00
:class="[{'size-down':(sizeDown == true), 'padded-bottom':extraToolbarsVisible }, 'position-'+position ]"
:style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}"
>
<!-- Loading indicator -->
<div v-if="loading" class="loading-note">
<div class="ui active dimmer">
<div class="ui text loader">{{loadingMessage}}</div>
</div>
</div>
2020-02-01 14:21:22 -08:00
<div class="note-menu">
<nm-button v-on:click.native="close" icon="close" />
2020-02-01 14:21:22 -08:00
<nm-button v-on:click.native="toggleList('ol')" icon="list ol" />
<nm-button v-on:click.native="toggleList('ul')" icon="tasks" />
<nm-button v-on:click.native="toggleBold()" icon="bold" />
<nm-button v-on:click.native="toggleItalic()" icon="quote left" />
2020-02-01 14:21:22 -08:00
<nm-button v-on:click.native="modifyFont('1.4em')" icon="text height" />
2020-02-01 14:21:22 -08:00
<nm-button v-on:click.native="undoCustom()" icon="undo" />
<nm-button v-if="usersOnNote > 1" icon="green user circle" />
<nm-button icon="ellipsis horizontal" v-on:click.native="showNoteOptions = !showNoteOptions" />
</div>
2020-02-01 14:21:22 -08:00
<div id="squire-id" class="squire-box" ref="squirebox"></div>
<!-- && this.$store.getters.getIsUserOnMobile -->
<span class="note-status-indicator" v-on:click="save()" v-if="statusText != 'Saved' && $store.getters.getIsUserOnMobile">
<div class="ui green button">{{statusText}}</div>
</span>
<!-- Note options on the bottom of note -->
<div class="all-settings" :class="{ 'low-settings':!extraToolbarsVisible }">
<div class="note-menu shrink-icons-on-mobile">
2020-02-01 14:21:22 -08:00
<!-- Pin Button -->
<nm-button
v-on:click.native="onToggleArchived"
:icon="(archived == 1)?'green archive':'archive'"
:text="(archived == 1)?'Archived':'Archive'"
tip="Show in archive"
:showText="true"
></nm-button>
2020-02-01 14:21:22 -08:00
<!-- archive button -->
<nm-button
v-on:click.native="onTogglePinned"
:icon="(pinned == 1)?'green pin':'pin'"
:text="(pinned == 1)?'Pinned':'Pin'"
tip="Pin to top of list"
:showText="true"
></nm-button>
2020-02-01 14:21:22 -08:00
<!-- colors button -->
<nm-button
v-on:click.native="showColorPicker"
icon="paint brush"
text="Colors"
tip="Colors"
></nm-button>
<!-- add images panel -->
<nm-button
v-on:click.native="showFilesSideMenu = !showFilesSideMenu"
icon="image"
text="Images"
tip="Images"
></nm-button>
<!-- Tags -->
<nm-button
v-on:click.native="showTagSlideMenu = !showTagSlideMenu; modified = true"
icon="tags"
text="Tags"
tip="Tags"
></nm-button>
2020-02-01 14:21:22 -08:00
<!-- file upload button -->
<file-upload-button
class="nm-button"
:noteId="noteid" />
<!-- files button -->
<nm-button
v-on:click.native="openEditAttachment"
icon="folder"
text="Files"
tip="Files on Note"
:showText="true"
></nm-button>
2020-02-01 14:21:22 -08:00
</div>
</div>
<!-- Side slide menus for colors, tags and images -->
<side-slide-menu v-if="colorPickerVisible" v-on:close="colorPickerVisible = false" name="colors">
<color-picker
@changeColor="onChangeColor"
@close="onCloseColorChanger"
:style-object="styleObject"
/>
</side-slide-menu>
<side-slide-menu v-if="showTagSlideMenu" v-on:close="showTagSlideMenu = false" 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="showFilesSideMenu" v-on:close="showFilesSideMenu = false" name="images" :style-object="styleObject">
<div class="ui basic segment">
<simple-attachment-note
v-on:close="showFilesSideMenu = false"
:note-id="noteid"
:squire-editor="editor">
</simple-attachment-note>
</div>
</side-slide-menu>
<side-slide-menu v-if="showNoteOptions" v-on:close="showNoteOptions = 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 items (Move checked to bottom)
</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 Items
</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 Checked items
</div>
</div>
<div class="eight wide column">
<div class="ui labeled icon fluid basic button" v-on:click="undoCustom">
<i class="undo icon"></i>
Undo last change
</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="sixteen wide column" v-if="rawTextId > 0">
<share-note-component
:note-id="noteid"
:raw-text-id="rawTextId"
:share-username="shareUsername"
/>
</div>
</div>
</div>
</side-slide-menu>
</div>
</template>
<script>
import axios from 'axios'
2020-02-01 14:21:22 -08:00
const DiffMatchPatch = require('../../../server/helpers/DiffMatchPatch')
export default {
name: 'InputNotes',
props: [ 'noteid', 'position' ],
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'),
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
},
data(){
return {
loading: true,
loadingMessage: 'Loading Note',
currentNoteId: 0,
modified: false,
noteText: '',
rawTextId: 0,
shareUsername: null,
2020-02-01 14:21:22 -08:00
diffNoteText: '',
statusText: 'Saved',
lastNoteHash: null,
saveDebounce: null, //Prevent save from being called numerous times quickly
updated: 'Never',
editDebounce: null,
2020-02-01 14:21:22 -08:00
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
colorPickerLocation: null,
tinymce: null, //Initialized editor instance
//Settings vars
showAllSettings: true,
lastVisibilityState: null,
2020-02-01 14:21:22 -08:00
//All the squire settings
editor: null,
// pastFocusedNode: null,
usersOnNote: 0,
extraToolbarsVisible: true,
showTagSlideMenu: false,
colorPickerVisible: false,
showFilesSideMenu: false,
showNoteOptions: false,
}
},
watch: {
noteid:function(newVal, oldVal){
if(newVal == this.currentNoteId){
return
}
if(newVal == oldVal){
return
}
this.currentNoteId = newVal
this.loadNote(this.currentNoteId)
}
},
beforeMount(){
2020-02-01 14:21:22 -08:00
this.$bus.$on('new_file_upload', ({noteId, imageCode}) => {
if(this.noteid == noteId && this.editor){
this.editor.moveCursorToEnd()
this.editor.insertHTML(imageCode)
this.save()
2020-02-01 14:21:22 -08:00
}
})
},
beforeDestroy(){
this.$io.emit('leave_room', this.rawTextId)
2020-02-01 14:21:22 -08:00
document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
2020-02-01 14:21:22 -08:00
this.editor.destroy()
2020-02-01 14:21:22 -08:00
this.$bus.$off('new_file_upload')
},
mounted: function() {
document.addEventListener('visibilitychange', this.checkForUpdatedNote)
this.$nextTick(() => {
2020-02-01 14:21:22 -08:00
this.loadNote(this.noteid)
})
},
methods: {
2020-02-01 14:21:22 -08:00
initSquire(){
//Set up squire and load note text
this.editor = new Squire( this.$refs.squirebox, {blockTag: 'p' })
this.setText(this.noteText)
//Click Event - Open links when clicked in editor or toggle checks
2020-02-01 14:21:22 -08:00
this.editor.addEventListener('click', e => {
2020-02-01 14:21:22 -08:00
//Link clicked in editor - open link
if(e.target.nodeName == 'A' && e.target.href){
window.open(e.target.href)
}
2020-02-01 14:21:22 -08:00
//List Item clicked in editor - toggle link state
if(e.target.nodeName == 'LI'){
2020-02-01 14:21:22 -08:00
let el = e.target
2020-02-01 14:21:22 -08:00
//Adjust ofset by 40 px
let correction = 40
2020-02-01 14:21:22 -08:00
//Determine if element was clicked or area before it, before means checkbox was clicked
if (e.offsetX > e.target.offsetLeft - correction) {
//Element was clicked
} else {
2020-02-01 14:21:22 -08:00
//Will hide keyboard if clicked, much better for mobile
this.editor.blur()
2020-02-01 14:21:22 -08:00
//Area before element was clicked, they clicked the checkbox
this.onKeyup()
if (el.className === 'active'){
el.className = 'inactive';
} else {
el.className = 'active';
}
2020-02-01 14:21:22 -08:00
}
}
})
this.editor.addEventListener('keydown', event => {
//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 => {
if(this.$store.getters.getIsUserOnMobile){
this.extraToolbarsVisible = false
}
})
2020-02-01 14:21:22 -08:00
this.editor.addEventListener('blur', e => {
this.save()
this.extraToolbarsVisible = true
})
},
2020-02-01 14:21:22 -08:00
//If nothing is selected, select the entire line
selectLineIfNoSelect(){
2020-02-01 14:21:22 -08:00
//Select entire line if range is not set
let selection = this.editor.getSelection()
2020-02-01 14:21:22 -08:00
if(selection.startOffset == selection.endOffset && selection.startContainer == selection.endContainer){
2020-02-01 14:21:22 -08:00
let squireRange = this.editor.createRange(
selection.startContainer, 0,
selection.endContainer, selection.commonAncestorContainer.textContent.length)
2020-02-01 14:21:22 -08:00
this.editor.setSelection(squireRange)
}
},
modifyFont(inSize){
2020-02-01 14:21:22 -08:00
this.selectLineIfNoSelect()
2020-02-01 14:21:22 -08:00
let fontInfo = this.editor.getFontInfo()
//Toggle font size between large and normal
if(fontInfo.size){
this.editor.setFontSize(null)
} else {
this.editor.setFontSize(inSize)
}
},
2020-02-01 14:21:22 -08:00
toggleList(type){
//Undo list if its already a lits
if(this.editor.hasFormat(type)){
this.editor.removeList()
return
}
2020-02-01 14:21:22 -08:00
if(type == 'ol'){
this.editor.makeOrderedList()
}
if(type == 'ul'){
this.editor.makeUnorderedList()
}
},
toggleBold(){
this.selectLineIfNoSelect()
if( this.editor.hasFormat('b') ){
this.editor.removeBold()
} else {
this.editor.bold()
}
},
toggleItalic(){
this.selectLineIfNoSelect()
if( this.editor.hasFormat('i') ){
this.editor.removeItalic()
} else {
this.editor.italic()
}
},
undoCustom(){
//The same as pressing CTRL + Z
// this.editor.focus()
// document.execCommand("undo", false, null)
this.editor.undo()
},
uncheckAllListItems(){
//
// Uncheck All List Items
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.showNoteOptions = false
}
//Fetch the container
let container = document.getElementById('squire-id')
Array.from( container.getElementsByClassName('active') ).forEach(item => {
item.classList.remove('active');
})
},
deleteCompletedListItems(){
//
// Delete Completed List Items
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.showNoteOptions = false
}
//Fetch the container
let container = document.getElementById('squire-id')
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Create two categories, done and not done list items
let undoneElements = document.createDocumentFragment()
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
//Check if the next item is a list, Keep lists with intented items together
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(!checkedItem){
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
})
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
}
})
},
sortList(){
//
// Sort list, checked at the bottom, unchecked at the top
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.showNoteOptions = false
}
//Fetch the container
let container = document.getElementById('squire-id')
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Create two categories, done and not done list items
let doneElements = document.createDocumentFragment()
let undoneElements = document.createDocumentFragment()
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
//Check if the next item is a list, Keep lists with intented items together
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(checkedItem){
doneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
doneElements.appendChild( sublist.cloneNode(true) )
}
} else {
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
})
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
node.appendChild(doneElements)
}
})
},
calculateMath(){
//
// Find math in note and calculate the outcome
//
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.showNoteOptions = false
}
//Fetch the container
let container = document.getElementById('squire-id')
// simple function that trys to evaluate javascript
const shittyMath = (string) => {
//Remove all chars but math chars
const cleanString = String(string).replace(/[a-zA-Z\s]*/g,'')
try {
return Function('"use strict"; return (' + cleanString + ')')();
} catch (error) {
console.log('Math Error: ', string)
return null
}
}
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
const line = node.innerText.trim()
// = sign exists and its the last character in the string
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
//Pull out everything before the formula and try to evaluate it
const formula = line.split('=').shift()
const output = shittyMath(formula)
//If its a number and didn't throw an error, update the line
if(!isNaN(output) && output != null){
//Since there is HTML in the line, splice in the number after the = sign
let equalLocation = node.innerHTML.indexOf('=')
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
newLine += ` ${output}`
newLine += node.innerHTML.slice(equalLocation+1).trim()
//Slam in that new HTML with the output
node.innerHTML = newLine
}
}
})
},
setText(inText){
2020-02-01 14:21:22 -08:00
this.editor.setHTML(inText)
this.noteText = this.editor._getHTML()
this.diffNoteText = this.editor._getHTML()
},
getText(){
2020-02-01 14:21:22 -08:00
return this.editor.getHTML()
},
showColorPicker(event){
this.colorPickerVisible = !this.colorPickerVisible
this.colorPickerLocation = {'x':event.clientX, 'y':event.clientY}
},
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()
},
2020-02-01 14:21:22 -08:00
onCloseColorChanger(){
2020-02-01 14:21:22 -08:00
this.colorPickerVisible = false
},
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){
let vm = this
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 = doing[Math.floor(Math.random() * doing.length)]
let p2 = thing[Math.floor(Math.random() * thing.length)]
vm.loadingMessage = p1 + ' ' + p2
//Component is activated with NoteId in place, lookup text with associated ID
if(this.$store.getters.getLoggedIn){
axios.post('/api/note/get', {'noteId': noteId})
.then(response => {
//Set up local data
vm.currentNoteId = noteId
this.rawTextId = response.data.rawTextId
this.shareUsername = response.data.shareUsername
2020-02-01 14:21:22 -08:00
vm.noteText = response.data.text
2020-02-01 14:21:22 -08:00
vm.diffNoteText = response.data.text
vm.updated = response.data.updated
vm.lastNoteHash = vm.hashString(response.data.text)
2020-02-01 14:21:22 -08:00
//Set up note colors
if(response.data.color){
2020-02-01 14:21:22 -08:00
vm.styleObject = JSON.parse(response.data.color)
}
if(response.data.pinned != null){
vm.pinned = response.data.pinned
}
vm.archived = response.data.archived
vm.attachmentCount = response.data.attachment_count
2020-02-01 14:21:22 -08:00
this.loading = false
vm.$nextTick(() => {
this.setupWebSockets()
2020-02-01 14:21:22 -08:00
this.initSquire()
})
})
} else {
console.log('Could not fetch note')
}
},
2020-02-01 14:21:22 -08:00
diffText(){
// dont emit to one user
if(this.usersOnNote <= 1){
return
}
//Post latest diff to server, server will emit change event to all connected clients
// clearTimeout(this.emitChangeDebounce)
this.emitChangeDebounce = setTimeout(i => {
//caldulate text diff
let oldText = this.diffNoteText
let newText = this.getText()
if(oldText == newText){
return
}
const dmp = new DiffMatchPatch.diff_match_patch()
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);
var patches = dmp.patch_fromText(patch_text);
var results = dmp.patch_apply(patches, oldText);
const computedText = results[0]
//Save computed diff text
this.noteText = computedText
this.diffNoteText = computedText
if(patch_text == ''){
return
}
// console.log(patch_text)
this.$io.emit('note_diff', {
id: this.rawTextId,
diff: patch_text
2020-02-01 14:21:22 -08:00
})
}, 5)
},
patchText(patch_text){
console.log(patch_text)
//
// Capture x,y of caret and position into string
//
let currentSelection = this.editor.getSelection()
let lineText = currentSelection.startContainer.textContent
console.log(lineText)
let cursorOffset = parseInt(currentSelection.startOffset) //number of characters in
let path = this.xpath(currentSelection.commonAncestorContainer.parentElement)
console.log(path)
//
//Set up text to process diff
//
let currentText = this.editor._getHTML()
const startingLines = (currentText.match(/<br>/g) || '').length + 1
console.log('1')
const dmp = new DiffMatchPatch.diff_match_patch()
var patches = dmp.patch_fromText(patch_text);
var results = dmp.patch_apply(patches, currentText);
let newText = results[0]
console.log('2')
this.noteText = newText
this.diffNoteText = newText
console.log('3')
// this.editor._setHTML(newText)
this.editor.setHTML(newText)
2020-02-01 14:21:22 -08:00
console.log('4')
//
// I user hasn't selected the document, we are done here
// @TODO add code to halt execution
//
const endingLines = (newText.match(/<br>/g) || '').length + 1
// if(this.pastFocusedNode != null || true){
setTimeout( ()=>{
var root = this.editor.getRoot()
//Get node under current x,y on dom (may break on scroll)
// let node = document.elementFromPoint(mouse.x, mouse.y)
let node = this.getElementByXPath(path)
if(node.firstChild){
node = node.firstChild
}
//If the number of lines changed
if(startingLines != endingLines){
//Line diff may be +1 or -1
let lineDiff = endingLines - startingLines
console.log('Line Diff => ', lineDiff)
//Pull out node number from path
var nodeNumber = path.match(/\d+/)
let modifyNode = null
if(nodeNumber.length == 1){
modifyNode = parseInt(nodeNumber[0])
}
path = path.replace(modifyNode, modifyNode + lineDiff )
console.log(path)
let maybeNext = this.getElementByXPath(path)
if(maybeNext && maybeNext.firstChild){
maybeNext = maybeNext.firstChild
}
if(maybeNext && maybeNext.textContent == lineText){
node = maybeNext
console.log('The Node Moved!')
}
}
console.log('Targeting Node')
console.log(node)
//Create and set range
let squireRange = this.editor.createRange(node, cursorOffset)
squireRange.collapse(true)
this.editor.setSelection(squireRange)
console.log('cursor set')
}, 20)
// }
},
xpath(el) {
//Skip things we can't use
if (typeof el == "string") return document.evaluate(el, document, null, 0, null)
if (!el || el.nodeType != 1) return ''
//Anchor xpath using Ids or test-ids
const testId = el.getAttribute('test-id')
if (el.id) return "//*[@id='" + el.id + "']"
//Continue to build path
const sames = [].filter.call(el.parentNode.children, function (x) { return x.tagName == el.tagName })
return this.xpath(el.parentNode) + '/' + el.tagName.toLowerCase() + (sames.length > 1 ? '['+([].indexOf.call(sames, el)+1)+']' : '')
},
getElementByXPath(xpath){
2020-02-01 14:21:22 -08:00
return new XPathEvaluator()
.createExpression(xpath)
.evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE) .singleNodeValue
},
onKeyup(event){
2020-02-01 14:21:22 -08:00
this.statusText = 'Save'
this.diffText()
//Each note, save after 5 seconds, focus lost or 30 characters typed.
clearTimeout(this.editDebounce)
this.editDebounce = setTimeout(() => {
this.save()
}, 5000)
//Save after 30 keystrokes
this.keyPressesCounter = (this.keyPressesCounter + 1)
if(this.keyPressesCounter > 30){
this.keyPressesCounter = 0
this.save()
}
},
save(){
return new Promise((resolve, reject) => {
//Clear other debounced events to prevent double calling of save
// clearTimeout(this.editDebounce)
2020-02-01 14:21:22 -08:00
// return resolve(true)
//Don't save note if its hash doesn't change
const currentNoteText = this.getText()
if( this.lastNoteHash == this.hashString( currentNoteText )){
this.statusText = 'Saved'
return resolve(true)
}
//If user accidentally clears note, it won't delete it
if(currentNoteText == ''){
this.statusText = 'Empty'
console.log('Prevented from saving empty note.')
return resolve(true)
}
const postData = {
'noteId':this.currentNoteId,
'text': currentNoteText,
'color': JSON.stringify(this.styleObject), //Save little json color object
'pinned': this.pinned,
'archived':this.archived,
}
this.statusText = 'Saving'
axios.post('/api/note/update', postData).then( response => {
this.statusText = 'Saved'
this.updated = Math.round((+new Date)/1000)
this.modified = true
//Update last saved note hash
this.lastNoteHash = this.hashString( currentNoteText )
return resolve(true)
})
})
},
checkForUpdatedNote(){
2020-02-01 14:21:22 -08:00
// 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.')
2020-02-01 14:21:22 -08:00
const postData = {
noteId:this.currentNoteId,
text:this.getText(),
updated: this.updated
}
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
}
})
}
//Track visibility state
this.lastVisibilityState = document.visibilityState
},
hashString(text){
var hash = 0;
2020-02-01 14:21:22 -08:00
if (text == null || text.length == 0) {
return hash;
}
//Simplified for speed
2020-02-01 14:21:22 -08:00
// return text.length
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;
},
close(){
// this.loading = true
// this.loadingMessage = 'Save and Close'
this.save().then( result => {
this.sizeDown = true
//This timeout allows animation to play before closing
setTimeout(() => {
this.$bus.$emit('close_active_note', {
position: this.position, noteId: this.noteid, modified: this.modified
})
return
}, 300)
})
},
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
})
//Server will hand deliver diffs from other notes to this one
this.$io.on('incoming_diff', incomingDiffData => {
this.patchText(incomingDiffData)
})
}
}
}
</script>
<style type="text/css" scoped>
2020-02-01 14:21:22 -08:00
/* squire styles */
/*Settings manager styles */
.all-settings {
2020-02-01 14:21:22 -08:00
/*border-top: 1px solid #534c68;*/
background: #221f2b;
position: absolute;
2020-02-01 14:21:22 -08:00
bottom: 0;
right: 0;
left: 0;
z-index: 99;
2020-02-01 14:21:22 -08:00
/*border: 1px solid;*/
/*background-color: var(--background_color);*/
/*border-color: var(--border_color);*/
box-sizing: border-box;
2020-02-01 14:21:22 -08:00
/*border-radius: 7px;*/
/*box-shadow: 0px 3px 7px 0px rgba(140,140,140,1);*/
/*padding: 1.2em 0 0;*/
}
.low-settings {
bottom: 0px;
cursor: pointer;
height: 1.4em;
padding-top: 1.5em;
overflow: hidden;
border: 1px solid #534c68;
}
/*End Settings manager styles */
/* container styles change based on mobile and number of open screens */
.master-note-edit {
position: fixed;
bottom: 0;
background: var(--background_color);
/*color: var(--text_color);*/
height: 100vh;
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
z-index: 1001;
/*overflow-x: scroll;*/
}
2020-02-01 14:21:22 -08:00
.padded-bottom {
padding-bottom: 20px;
}
.loading-note {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* One note open, in the middle of the screen */
.master-note-edit.position-0 {
left: 50%;
right: 0;
}
@media only screen and (max-width: 740px) {
.master-note-edit.position-0 {
left: 0;
right: 0;
top: 0;
bottom: 0;
}
}
/* 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%;
}
.size-down {
animation: size-down 0.5s ease;
}
@keyframes size-down {
0% {
/*opacity: 1;*/
/*top: 0;*/
top: 0;
}
100% {
/*opacity: 0;*/
/*top: 30vh;*/
top: 150vh;
}
}
</style>