<template> <!-- change class to .master-note-edit to have it popup on the screen. @keyup.esc="closeButtonAction()" --> <div class="master-note-edit"> <!-- Edit Menus --> <div class="menu-top-half" :class="{ 'hide-text':(openNotes > 1) }"> <div class="edit-button" v-on:click="colorpicker = true" data-tooltip="Text Color" data-position="bottom center"> <i class="font icon"></i> <div class="font-color-bar" :style="{'background':lastUsedColor}"></div> <span>Color</span> </div> <div class="edit-button" v-on:click="toggleBold()" :data-tooltip="`Bold\n(CTRL + b)`" data-position="bottom center" :class="{'edit-active':activeBold}"> <i class="bold icon"></i> <span>Bold</span> </div> <div class="edit-button" v-on:click="toggleItalic()" :data-tooltip="`Italic\n(CRTL + i)`" data-position="bottom center" :class="{'edit-active':activeItalics}"> <i class="italic icon"></i> <span>Italic</span> </div> <div class="edit-button" v-on:click="toggleUnderline()" :data-tooltip="`Underline\n(CRTL + u)`" data-position="bottom center" :class="{'edit-active':activeUnderline}"> <i class="underline icon"></i> <span>Underline</span> </div> <div class="edit-button" v-on:click="modifyCode('1.4em')" data-tooltip="Quote" data-position="bottom center" :class="{'edit-active':activeCode}"> <i class="quote right icon"></i> <span>Quote</span> </div> <div class="edit-button" v-on:click="modifyFont('0.9em')" data-tooltip="Sub Title" data-position="bottom center" :class="{'edit-active':activeSubTitle}"> <i class="small text height icon"></i> <span>Small Text</span> </div> <div class="edit-button" v-on:click="modifyFont('1.4em')" data-tooltip="Title" data-position="bottom center" :class="{'edit-active':activeTitle}"> <i class="text height icon"></i> <span>Title</span> </div> <div class="edit-button" v-on:click="removeFormatting()" data-tooltip="Remove Formatting" data-position="bottom center"> <i class="remove format icon"></i> </div> <div class="edit-divide"></div> <div class="edit-button" v-on:click="indentText" :data-tooltip="`Indent\n(TAB)`" data-position="bottom center"> <i class="indent icon"></i> <span>Indent</span> </div> <div class="edit-button" v-on:click="outdentText" :data-tooltip="`Un-Indent\n(SHIFT + TAB)`" data-position="bottom center"> <i class="outdent icon"></i> </div> </div> <div class="menu-bottom-half" :class="{ 'hide-text':(openNotes > 1) }"> <!-- <div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/table`)" data-tooltip="Insert Table" data-position="bottom center"> <i class="border all icon"></i> </div> --> <div class="edit-button" v-on:click="toggleList('ul')" :data-tooltip="`Task List\n(CTRL + SHIFT + 8)`" data-position="top center" :class="{'edit-active':activeToDo}"> <i class="tasks icon"></i> <span>To-Do</span> </div> <div class="edit-button" v-on:click="toggleList('ol')" :data-tooltip="`Ordered List\n(CTRL + SHIFT + 9)`" data-position="top center" :class="{'edit-active':activeList}"> <i class="list ol icon"></i> <span>List</span> </div> <div class="edit-divide"></div> <div class="edit-button" v-on:click="insertDivide()" data-tooltip="Insert Divide" data-position="top center"> <i class="grip lines icon"></i> <span>Divide</span> </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="top center" :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="top center"> <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="top center"> <i class="image icon"></i> <span>Images</span> </div> <file-upload-button data-tooltip="Upload File" data-position="top center" 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="top center"> <i class="ellipsis horizontal icon"></i> </div> <div class="edit-divide"></div> <div class="edit-button" v-on:click="undoCustom()" :data-tooltip="`Undo\n(CTRL + z)`" data-position="top center"> <i class="reply icon"></i> </div> <div class="edit-button done-button" v-on:click="closeButtonAction()" :data-tooltip="`Close\n(ESC)`" data-position="top center"> <!-- <i class="green close icon"></i> --> <span class="ui green text">Done</span> </div> </div> <div class="input-container-wrapper" :class="{ 'side-menu-open':sideMenuOpen }"> <!-- Squire box grows --> <div class="note-wrapper"> <!-- 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 glint"> </textarea> <!-- close button giant --> <div v-if="!$store.getters.getIsUserOnMobile" class="large-close-button" v-on:click="closeButtonAction()"> <i class="fitted green close icon"></i> </div> <!-- tags on the side, only show on desktop --> <div class="note-mini-tag-area" v-on:click="$router.push(`/notes/open/${noteid}/menu/tags`)" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText'] }"> <span class="add-mini-tag" v-if="allTags.length == 0"> <i class="tags icon"></i>Add Tags </span> <span v-for="tag in allTags" class="active-mini-tag"> #{{ tag }} </span> <span class="active-mini-tag" v-if="allTags.length > 0"> + </span> <span class="status-menu" v-on:click=" hash=0; save()"> <span v-if="diffsApplied > 0"> +{{ diffsApplied }} Unsaved Changes </span> <span v-if="usersOnNote > 1" :data-tooltip="`Viewers`" data-position="left center"> <i class="green eye icon"></i> {{ usersOnNote }} </span> <span v-if="statusText == 'modified'" data-position="left center" data-tooltip="Modified"> <i class="grey asterisk icon"></i> </span> <span v-if="statusText == 'saving'" data-position="left center" data-tooltip="Saving"> <i class="grey upload icon"></i> </span> <span v-if="statusText == 'saved'" data-position="left center" data-tooltip="Saved"> <i class="grey check icon"></i> </span> </span> </div> <!-- Squire Box --> <div id="squire-id" class="squire-box" ref="squirebox" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText'] }" placeholder="Type Note Here"></div> </div> </div> <!-- color picker --> <color-tooltip v-if="colorpicker" :last-used-color="lastUsedColor" 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> --> <!-- tag edit menu --> <side-slide-menu v-if="tags" v-on:close="tags = false; fetchNoteTags()" name="tags"> <div class="ui basic segment"> <note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/> </div> </side-slide-menu> <!-- images menu --> <side-slide-menu v-if="images" v-on:close="images = false" name="images"> <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"> <div class="ui basic padded segment"> <div class="ui compact stackable grid"> <div class="eight wide column"> <div class="ui dividing header"> Note Options </div> <div class="ui labeled icon fluid basic button" v-on:click="onToggleArchived()"> <i class="archive icon" :class="{'green':(archived == 1)}"></i> <span v-if="archived == 1">Un-Archive Note</span> <span v-if="archived != 1">Archive Note</span> </div> <div class="ui labeled icon fluid basic button" v-on:click="onTogglePinned"> <i class="pin icon" :class="{'green':(pinned == 1)}"></i> <span v-if="pinned == 1">Un-Pin Note</span> <span v-if="pinned != 1">Pin Note</span> </div> </div> <div class="eight wide column"> <div class="ui dividing header"> List Options </div> <div class="ui labeled icon fluid basic button" v-on:click="sortList"> <i class="sort amount up icon"></i> Sort List (Complete to bottom) </div> <div class="ui labeled icon fluid basic button" v-on:click="uncheckAllListItems"> <i class="list ul icon"></i> Uncheck All </div> <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 dividing header"> Calculate Line </div> <p> Calculates algebra before '=' </p> <div class="ui labeled icon fluid basic button" v-on:click="calculateMath"> <i class="calculator icon"></i> Calculate Simple Math </div> </div> <div class="eight wide column"> <!-- data-tooltip="Files on note" --> <div class="ui dividing header"> Note Attachments & Links </div> <p> Attachment & Link Count {{ attachmentCount }} </p> <div v-on:click="openEditAttachment" class="ui labeled icon fluid basic button"> <i class="folder icon"></i> View all Attachments & Links </div> </div> <color-picker @changeColor="onChangeColor" @close="colors = false; $router.go(-1)" :style-object="styleObject" /> <div class="sixteen wide column" v-if="rawTextId > 0"> <div class="ui dividing header"> Share Note </div> <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;" 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="{ 'fade-me-out':sizeDown }" v-on:click="closeButtonAction()"></div> --> </div> </template> <script> const Squire = require('../assets/squire.js') 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: 'NoteInputPanel', props: [ 'noteid', 'position', 'openMenu', 'urlData', 'openNotes'], 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: false, 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', lastInteractionTimestamp:null, //Tracks when note was loaded and last saved/refreshed editDebounce: null, textChangedDebounce: null, keyPressesCounter: 0, //Determine 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 && newVal.id == this.noteid){ //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() } }) this.$bus.$on('close_note_by_id', (noteId) => { if(noteId == this.noteid){ this.closeButtonAction() } }) }, 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() // trigger save actions and reindex this.close() }, mounted: function() { //Show loading if note has not loaded in 500ms setTimeout(()=>{ this.forceShowLoading = true }, 500) document.addEventListener('visibilitychange', this.checkForUpdatedNote) //Init squire as early as possible if(this.editor && this.editor.destroy){ this.editor.destroy() } 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) }, fetchNoteTags(){ axios.post('/api/tag/fornote', {'noteId': this.noteid}) .then(({data}) => { //Setup note tags from string this.allTags = data.tags ? data.tags.split(',') : [] }) }, 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() } //Set up websockets after squire is set up setTimeout(() => { this.setupWebSockets() }, 500) this.editor.addEventListener('cursor', e => { //Save range to replace cursor if someone else makes an update this.lastRange = e.range this.startOffset = parseInt(e.range.startOffset) return }) //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){ //Area before element was clicked, they clicked the checkbox if (el.className === 'active'){ el.className = 'inactive'; } else { el.className = 'active'; } //Trigger keyup event to save list changes this.onKeyup(e) //Will hide keyboard if clicked on mobile if(this.$store.getters.getIsUserOnMobile){ setTimeout(() => { document.activeElement.blur() e.preventDefault() return }, 25) } } } }) 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.outdentText() } else { this.indentText() } 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) }) // this.editor.addEventListener("dragstart", e => { // console.log('Dragging') // console.log(e) // if(){} // }); //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','Diligently','','','','','','','','','','','','','',''] 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 } //Setup all responsive vue data this.setupLoadedNoteData(response) 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') } }, setupLoadedNoteData(response){ //All the data returned by the server, setup locally in vue component //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.lastInteractionTimestamp = +new Date 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 //Setup note tags this.allTags = response.data.tags ? response.data.tags.split(','):[] //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 return true }, //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() }, 5 * 1000) //Save after x keystrokes this.keyPressesCounter = (this.keyPressesCounter + 1) if(this.keyPressesCounter > 60){ 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.lastInteractionTimestamp = +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(){ const now = +new Date //Only check every 3 seconds const checkForUpdateTimeout = now - this.lastInteractionTimestamp > (2 * 1000) //If user leaves page then returns to page, reload the first batch if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible' && checkForUpdateTimeout){ //Focus Regained on Note, check for update axios.post('/api/note/get', { 'noteId': this.noteid }) .then(response => { const serverTextHash = this.hashString( response.data.text ) if(this.lastNoteHash != serverTextHash){ // console.log('note was changed UPDATE THAT BITCH!!!!') this.setupLoadedNoteData(response) //Manually set squire text to show this.setText(this.noteText) } }) } //Keep track of visibility change and last interaction time this.lastVisibilityState = document.visibilityState this.lastInteractionTimestamp = +new Date }, 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(playAnimation = false){ this.sizeDown = playAnimation const animationTimeout = (playAnimation ? 300 : 0) //This timeout allows animation to play before closing setTimeout(() => { // this.$router.push('/notes') this.close() }, animationTimeout) }, 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 if(element){ element.style.height = 'auto' element.style.height = (element.scrollHeight) +'px' } }, } } </script> <style type="text/css" scoped> .status-menu { position: absolute; right: 10px; z-index: 1019; text-align: right; } .font-color-bar { /*width: calc(100% - 8px);*/ height: 3px; position: absolute; bottom: 2px; right: 4px; left: 4px; z-index: 0; background: linear-gradient( 45deg, rgba(255, 0, 0, 1) 0%, rgba(208, 222, 33, 1) 20%, rgba(63, 218, 216, 1) 40%, rgba(28, 127, 238, 1) 60%, rgba(186, 12, 248, 1) 80%, rgba(255, 0, 0, 1) 100% ); overflow: hidden; } .large-close-button { position: absolute; min-height: 55px; top: 0px; right: 0px; font-size: 23px; display: flex; justify-content: center; align-items: center; padding: 0 20px 0 10px; cursor: pointer; } .full-focus-shade { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--small_element_bg_color); z-index: 999; cursor: pointer; opacity: 0.6; } /* squire styles */ .input-container-wrapper { position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; overflow-x: hidden; scrollbar-width: none; scrollbar-color: transparent transparent; overscroll-behavior: contain; background-color: var(--border_color); } .note-wrapper { background-color: var(--border_color); position: relative; margin: 50px auto; max-width: 1100px; } .note-mini-tag-area { width: 100%; padding: 5px 15px 0 15px; cursor: pointer; position: relative; background: var(--small_element_bg_color); } .add-mini-tag { color: var(--border_color); } .active-mini-tag { display: inline-block; opacity: 0.9; color: var(--main-accent); } .active-mini-tag + .active-mini-tag { margin-left: 15px; } @keyframes blinker { 50% { opacity: 0.2; } } /* Edit Menu Styles START */ .menu-top-half, .menu-bottom-half { display: flex; justify-content: center; position: absolute; z-index: 1001; background-color: green; border-radius: 3px; padding: 5px 5px; background-color: var(--menu-background); left: 0; right: 0; } .menu-top-half{ top: 0; } .menu-bottom-half { bottom: 0; } .menu-top-half.hide-text .edit-button > span:not(.ui), .menu-bottom-half.hide-text .edit-button > span:not(.ui) { display: none; } .edit-button { background-color: var(--small_element_bg_color); color: var(--menu-text); display: inline-block; border-radius: 3px; cursor: pointer; font-size: 1em; box-shadow: 0 0 1px 0 #c4c4c4; margin: 0 3px 0; padding: 6px 12px 0; text-align: center; min-width: 25px; min-height: 30px; /*flex-basis: 100%;*/ white-space: nowrap; } .edit-button > i { font-size: 1em; padding: 0; margin: 0; } .edit-button > span { padding: 0 8px; } .edit-button:hover { background-color: var(--menu-accent); } .edit-active { background-color: var(--main-accent); color: white; } .edit-divide { display: inline-block; height: 15px; width: 7px; padding: 0; } @media only screen and (max-width: 740px) { .edit-button { font-size: 1.2em; } .menu-top-half, .menu-bottom-half { padding: 3px 2px; left: 0; right: 0; transform: none; border-radius: 0; } .menu-bottom-half { position: fixed; z-index: 100000; } .note-wrapper { margin-bottom: 100px; margin-top: 38px; } } /* Edit Menu Styles END */ .stealth-input { width: 100%; padding: 15px; background-color: var(--small_element_bg_color ); border: none; border-bottom: 2px solid var(--main-accent); font-size: 1.7em; color: var(--text_color); caret-color: var(--main-accent); resize: none; overflow: hidden; /*margin: 0;*/ outline: none; display: block; margin-left: auto; margin-right: auto; max-width: 1100px; } /*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; left: 15%; right: 15%; 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 */ .side-menu-open { left: calc(50% + 10px) !important; right: calc(0% + 10px) !important; } /*weird inbetween size for tables*/ @media only screen and (max-width: 875px) { } @media only screen and (max-width: 830px) { .shade1, .shade2 { right: 150%; } .edit-divide { display: none; } .edit-button { padding: 6px 0px 0; } .edit-button > span:not(.ui) { display: 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; } } .fade-me-out { animation: fade-me-out 0.5s ease; } @keyframes fade-me-out { 0% { opacity: 1; } 100% { opacity: 0; } } .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>