<template> <!-- change class to .master-note-edit to have it popup on the screen --> <div id="InputNotes" class="master-note-edit" @keyup.esc="close" :class="[{ 'size-down':(sizeDown == true), 'full-focus':(fullFocusEditor) }, 'position-'+position ]" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}" > <div class="input-container-wrapper"> <!-- Loading indicator --> <div v-if="loading" class="loading-note"> <div class="ui active dimmer"> <div class="ui text loader">{{loadingMessage}}</div> </div> </div> <div class="note-menu"> <nm-button v-on:click.native="close" icon="close" /> <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" /> <nm-button v-on:click.native="modifyFont('1.4em')" icon="text height" /> <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> <!-- Squire box grows --> <div class="note-wrapper"> <textarea ref="titleTextarea" v-on:keyup="titleResize" v-on:keydown="titleResize" @keydown.enter.exact.prevent="editor.focus()" 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> <div v-show="isDecrypted" id="squire-id" class="squire-box" ref="squirebox" placeholder="Note Text"></div> <!-- Decrypt note prompt --> <div v-if="isEncrypted && !isDecrypted" class="ui basic padded segment"> <div class="ui raised segment"> <h3 class="ui center aligned icon header"> <i class="green lock alternate icon"></i> <span v-if="!lockedOut" :data-tooltip="`Unlock Attempts: ${decryptAttempts}`"> This note is encrypted and requires a password to be opened. </span> <!-- note is locked for 5 minutes --> <span v-if="lockedOut"> To many unlock attempts. Note is locked for 5 minutes. </span> </h3> <!-- Decrypt note --> <div class="ui form" v-if="!lockedOut"> <h5 class="ui horizontal divider header" v-if="passwordHint && passwordHint.length > 0"> Hint: {{ passwordHint }} </h5> <div class="field"> <input :name="`randomThing-${noteid}`" :id="`yupper-${noteid}`"type="password" v-model="password" placeholder="Note Password" v-on:keyup.enter="decryptNote" autofocus ref="decryptNotePrompt"> </div> <div class="field"> <div v-on:click="decryptNote" class="ui green fluid button" v-if="password.length >= 3"> Unlock Note </div> <div class="ui disabled fluid button" v-if="password.length < 3"> Unlock Note </div> </div> </div> </div> </div> <!-- bottom stats --> <div class="ui basic segment"> <div class="ui grid"> <div class="sixteen wide column"> <div class="ui basic button"v-if="!isEncrypted" v-on:click="passwordEnterVisible = true"> <i class="shield alternate icon"></i> Password Protect </div> <div class="ui icon basic button" v-if="isEncrypted && isDecrypted" v-on:click="disableEncryption"> <i class="unlock icon"></i> Remove Password </div> <div class="ui basic compact button"> Status: {{ statusText }} </div> <div class="ui basic compact button" :data-tooltip="`Created: ${$helpers.timeAgo(created)}`"> Last Change: {{ $helpers.timeAgo(updated) }} </div> </div> </div> </div> </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"> <!-- 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> <!-- 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> <!-- 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> <!-- 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> </div> </div> </div> <!-- Side slide menus for colors, tags, images and other options --> <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> <side-slide-menu v-if="passwordEnterVisible" v-on:close="passwordEnterVisible = false" :fullShadow="true" name="encrypt note"> <div class="ui basic segment" v-if="isDecrypted && isEncrypted"> <p>Note Decrypted</p> <div class="ui green button" v-on:click="lockNote">Lock Note</div> </div> <div v-if="!isEncrypted" class="ui basic segment"> <div class="ui top attached segment"> <h2><i class="green lock alternate icon"></i>Password protect this Note</h2> <p>Password protection will prevent anyone from reading the text of this note, unless they enter the correct password.</p> <p><b>Only the note text is protected. Title, tags, and files are not encrypted and remain visible without a password.</b></p> <p>The password you select will only be used for this note. You can use the same password on multiple notes. The note will be encrypted using the password entered. A longer password is will be more secure.</p> <h4><i class="red icon exclamation triangle"></i> Warning. There is no way to recover a lost password.</h4> </div> <div class="ui bottom attached segment"> <div class="ui form"> <div class="field"> <label>New Password to lock this note</label> <input :name="`randomThing-${noteid}`" :id="`yupper-${noteid}`"type="password" v-model="password" placeholder="Note Password"> </div> <div class="field" v-if="password.length > 3"> <label>Confirm Password</label> <input :name="`randomStuff-${noteid}`" :id="`heckye-${noteid}`"type="password" v-model="passwordConfirm" placeholder="Confirm Password"> </div> <div class="field" v-if="password.length > 3"> <label>Password Hint - visible when unlocking note</label> <input :name="`randomStuzz-${noteid}`" :id="`heckyo-${noteid}`"type="text" v-model="passwordHint" placeholder="Optional Password Hint" v-on:keyup.enter="enableEncryption"> </div> <div class="field" v-if="passwordConfirm.length > 3 && password != passwordConfirm"> <div v-on:click="enableEncryption" class="ui disabled green button"> Passwords do not match </div> </div> <div class="field" v-if="passwordConfirm.length > 3 && password == passwordConfirm"> <div v-on:click="enableEncryption" class="ui green button"> Protect! </div> </div> </div> </div> </div> </side-slide-menu> <!-- <div class="full-focus-shade"></div> --> </div> </template> <script> import axios from 'axios' const crypto = require('crypto') 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: '', 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 colorPickerLocation: null, fullFocusEditor: true, //Initialized editor instance //Settings vars showAllSettings: true, lastVisibilityState: null, //All the squire settings editor: null, // pastFocusedNode: null, usersOnNote: 0, extraToolbarsVisible: true, showTagSlideMenu: false, colorPickerVisible: false, showFilesSideMenu: false, showNoteOptions: false, //Encryption options passwordHint: '', password: '', //Field Variables, only for form passwordConfirm: '', //Only a form variable hashedPass: '', //sha-256 password hash, sends to server for decryption isEncrypted: false, isDecrypted: false, passwordEnterVisible: false, decryptAttempts: 0, lockedOut: false, autoLockTimeout: null, } }, watch: { noteid:function(newVal, oldVal){ if(newVal == this.currentNoteId){ return } if(newVal == oldVal){ return } this.currentNoteId = newVal this.loadNote(this.currentNoteId) } }, 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.password = '' this.passwordConfirm = '' this.hashedPass = '' clearTimeout(this.autoLockTimeout) this.$io.emit('leave_room', this.rawTextId) document.removeEventListener('visibilitychange', this.checkForUpdatedNote) this.editor.destroy() this.$bus.$off('new_file_upload') }, mounted: function() { document.addEventListener('visibilitychange', this.checkForUpdatedNote) this.$nextTick(() => { this.loadNote(this.noteid) }) }, methods: { initSquire(){ //Set up squire and load note text this.editor = new Squire( this.$refs.squirebox, {blockTag: 'p' }) this.setText(this.noteText) //focus on open, not on mobile, thats annoying if(!this.$store.getters.getIsUserOnMobile){ // this.editor.focus() if(this.noteTitle && this.noteTitle.length == 0){ this.$refs.titleTextarea.focus() } else { this.editor.moveCursorToEnd() } } //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 //Adjust ofset by 40 px let correction = 40 //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 { //Will hide keyboard if clicked, much better for mobile this.editor.blur() //Area before element was clicked, they clicked the checkbox this.onKeyup() if (el.className === 'active'){ el.className = 'inactive'; } else { el.className = 'active'; } } } }) 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 } }) this.editor.addEventListener('blur', e => { this.save() this.extraToolbarsVisible = true }) }, //If nothing is selected, select the entire line selectLineIfNoSelect(){ //Select entire line if range is not set let selection = this.editor.getSelection() if(selection.startOffset == selection.endOffset && selection.startContainer == selection.endContainer){ let squireRange = this.editor.createRange( selection.startContainer, 0, selection.endContainer, selection.commonAncestorContainer.textContent.length) this.editor.setSelection(squireRange) } }, modifyFont(inSize){ this.selectLineIfNoSelect() let fontInfo = this.editor.getFontInfo() //Toggle font size between large and normal if(fontInfo.size){ this.editor.setFontSize(null) } else { this.editor.setFontSize(inSize) } }, toggleList(type){ //Undo list if its already a lits if(this.editor.hasFormat(type)){ this.editor.removeList() return } 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){ this.editor.setHTML(inText) this.noteText = this.editor._getHTML() this.diffNoteText = this.editor._getHTML() }, getText(){ 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() }, onCloseColorChanger(){ 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){ //Generate a random loading message 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)] this.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': this.noteid, 'password':this.hashedPass }) .then(response => { //Set up local data this.currentNoteId = this.noteid this.rawTextId = response.data.rawTextId this.shareUsername = response.data.shareUsername this.passwordHint = response.data.password_hint this.created = response.data.created this.updated = response.data.updated this.noteTitle = response.data.title this.noteText = response.data.text this.diffNoteText = response.data.text this.lastNoteHash = this.hashString(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.isDecrypted = response.data.decrypted this.isEncrypted = response.data.encrypted == 1 this.decryptAttempts = response.data.decrypt_attempts_count this.lockedOut = response.data.lockedOut //If password is required, display a prompt and focus on it if(this.password.length == 0 && this.isEncrypted && !this.isDecrypted){ this.$nextTick(() => { if(this.$refs.decryptNotePrompt){ // this.editor.moveCursorToEnd() this.$refs.decryptNotePrompt.focus() } }) } this.$nextTick(() => { //Adjust note title size after load this.titleResize() this.setupWebSockets() this.initSquire() this.startAutolockTimer() }) }) } else { console.log('Could not fetch note') } }, 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 }) }, 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) 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){ return new XPathEvaluator() .createExpression(xpath) .evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE) .singleNodeValue }, onKeyup(){ 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) // return resolve(true) //Encrypted notes that are not decrypted should not be saved if(this.isEncrypted && !this.isDecrypted){ 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, 'title': this.noteTitle, 'text': currentNoteText, 'color': JSON.stringify(this.styleObject), //Save little json color object 'pinned': this.pinned, 'archived': this.archived, 'password': this.hashedPass, 'hint': this.passwordHint, } this.statusText = 'Saving' axios.post('/api/note/update', postData).then( response => { this.statusText = 'Saved' this.updated = Math.round((+new Date)/1000) this.modified = true console.log('Saved') //Update last saved note hash this.lastNoteHash = this.hashString( currentNoteText ) this.startAutolockTimer() return resolve(true) }) }) }, checkForUpdatedNote(){ // 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 } 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){ text = this.noteTitle + text var hash = 0; if (text == null || text.length == 0) { return hash; } //Simplified for speed // 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) }) }, decryptNote(){ const hashed = crypto.createHash('sha256').update(this.password).digest().toString('base64') //Remove plaintext password this.hashedPass = hashed this.password = '' this.passwordConfirm = '' this.loadNote() }, lockNote(){ this.save().then(results => { this.isDecrypted = false this.password = '' this.hashedPass = '' this.passwordEnterVisible = false this.setText('') }) }, enableEncryption(){ if(this.noteText == ''){ this.noteText = 'Text Typed here is encrypted.' } const hashed = crypto.createHash('sha256').update(this.password).digest().toString('base64') //Remove plaintext password this.hashedPass = hashed this.lastNoteHash = 0 this.password = '' this.passwordConfirm = '' this.passwordEnterVisible = false this.save() .then(results => { this.$bus.$emit('notification', 'Password Protection Enabled') this.loadNote() }) }, disableEncryption(){ this.lastNoteHash = 0 this.isEncrypted = false this.password = '' this.passwordConfirm = '' this.hashedPass = '' this.passwordEnterVisible = false //Reload Note this.save() .then(results => { this.loadNote() this.$bus.$emit('notification', 'Password Protection Removed') }) }, titleResize(){ //Resize the title field let element = this.$refs.titleTextarea let padding = 0 element.style.height = 'auto'; element.style.height = (element.scrollHeight + padding) +'px'; }, startAutolockTimer(){ //Start autolock timer on encrypted notes that are encrypted and in a decrypted state if(this.isEncrypted && this.isDecrypted){ clearTimeout(this.autoLockTimeout) this.autoLockTimeout = setTimeout(() => { this.lockNote() }, (60 * 1000 * 20) ) //Autolock after 20 min } }, } } </script> <style type="text/css" scoped> /* squire styles */ .input-container-wrapper { display: block; height: 100%; width: 100%; margin: 0; padding: 0; overflow: hidden; display: flex; flex-direction: column; } /*Three main elements nested in panel */ .note-menu { /*position: absolute;*/ top: 0;/* left: 0; right: 0;*/ flex-grow: 0; } .note-wrapper { flex-grow: 1; overflow: scroll; } .stealth-input { width: 100%; padding: 10px 15px 5px; background-color: rgba(255,255,255,0.1); border: none; font-size: 1.7em; /*line-height: 1.7em;*/ color: var(--text_color); resize: none; overflow: hidden; } /*Settings manager styles */ .all-settings { /*border-top: 1px solid #534c68;*/ background: #221f2b; /*position: absolute;*/ /*bottom: 40px;*/ /*right: 0;*/ /*left: 0;*/ z-index: 99; /*border: 1px solid;*/ /*background-color: var(--background_color);*/ /*border-color: var(--border_color);*/ /*box-sizing: border-box;*/ /*border-radius: 7px;*/ /*box-shadow: 0px 3px 7px 0px rgba(140,140,140,1);*/ /*padding: 1.2em 0 0;*/ flex-grow: 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;*/ overflow-y: scroll; overflow-x: hidden; } .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-1.full-focus { left: 20%; right: 20%; } .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; } .size-down { animation: size-down 0.5s ease; } @keyframes size-down { 0% { top: 0; } 100% { top: 150vh; } } </style>