SolidScribe/client/src/pages/NotesPage.vue

729 lines
21 KiB
Vue
Raw Normal View History

<template>
<div class="ui basic segment no-fluf-segment">
<div class="ui grid" :class="{ 'mush-it-up':showOneColumn() }" ref="content">
<div class="sixteen wide column">
<!-- :class="{ 'sixteen wide column':showOneColumn(), 'sixteen wide column':!showOneColumn() }" -->
<div class="ui grid">
<div class="six wide column" v-if="!$store.getters.getIsUserOnMobile">
<search-input></search-input>
</div>
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
<div class="ui basic button"
v-on:click="updateFastFilters(3)"
v-if="$store.getters.totals && ($store.getters.totals['sharedToNotes'] > 0 || $store.getters.totals['sharedFromNotes'] > 0)"
style="position: relative;">
<i class="green mail icon"></i>Shared Notes
<span class="floating ui green label" v-if="$store.getters.totals['unreadNotes'] > 0">
{{ $store.getters.totals['unreadNotes'] }}
</span>
</div>
<div class="ui basic button" v-on:click="updateFastFilters(2)" v-if="$store.getters.totals && $store.getters.totals['archivedNotes'] > 0">
<i class="green archive icon"></i>Archived
<!-- <span>{{ $store.getters.totals['archivedNotes'] }}</span> -->
</div>
</div>
<div class="eight wide column" v-if="showClear">
<!-- <fast-filters /> -->
<span class="ui fluid green button"
@click="reset">
<i class="arrow circle left icon"></i>Back to All Notes
</span>
</div>
</div>
</div>
<h2 v-if="fastFilters['withLinks'] == 1">Notes with Links</h2>
<h2 v-if="fastFilters['withTags'] == 1">Notes with Tags</h2>
<h2 v-if="fastFilters['onlyArchived'] == 1">Archived Notes</h2>
<h2 v-if="fastFilters['onlyShowSharedNotes'] == 1">Shared Notes</h2>
<div v-if="commonTags.length > 0" class="sixteen wide column">
<h4><i class="green tags icon"></i>Tags</h4>
<span v-for="tag in commonTags" @click="toggleTagFilter(tag.id)">
<span class="ui clickable basic label" :class="{ 'green':(searchTags.includes(tag.id)) }">
{{ucWords(tag.text)}} <span class="detail">{{tag.usages}}</span>
</span>
</span>
</div>
<!-- Note title card display -->
<div class="sixteen wide column">
<h3 v-if="searchTerm.length > 0 && notes.length == 0">No notes found. Check your spelling, try completing the word or using a different phrase.</h3>
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0">
No Notes Yet. Create one when you feel ready.
</h3>
<!-- <div v-if="working">
<div class="ui active inline loader"></div> Working...
</div> -->
<!-- Go to one wide column, do not do this on mobile interface -->
<div v-if="notes !== null && notes.length > 0"
:class="{'one-column':(
(activeNoteId1 != null || activeNoteId2 != null) &&
!$store.getters.getIsUserOnMobile
)}">
<!-- pinned notes -->
<div v-if="containsPinnednotes > 0" class="note-card-section">
<!-- ({{containsPinnednotes}}) -->
<h4><i class="green pin icon"></i>Pinned</h4>
<div class="note-card-display-area">
<note-title-display-card
v-for="note in notes"
v-if="note.pinned"
:onClick="openNote"
:data="note"
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length"
/>
</div>
</div>
<!-- normal notes -->
<div v-if="containsNormalNotes > 0" class="note-card-section">
<!-- ({{containsNormalNotes}}) -->
<h4><i class="green file icon"></i>Notes</h4>
<div class="note-card-display-area">
<note-title-display-card
v-for="note in notes"
v-if="note.note_highlights && !note.pinned"
:onClick="openNote"
:data="note"
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length"
/>
</div>
</div>
<!-- found in text -->
<div v-if="containsTextResults" class="note-card-section">
<h4><i class="green paragraph icon"></i> Found in Text ({{containsTextResults}})</h4>
<div class="note-card-display-area">
<note-title-display-card
v-for="note in notes"
v-if="note.note_highlights && note.note_highlights.length"
:textResults="true"
:onClick="openNote"
:data="note"
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length"
/>
</div>
</div>
</div>
</div>
2020-02-01 22:21:22 +00:00
<!-- found attachments -->
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
<h4><i class="folder open outline icon"></i> Found in Files ({{ foundAttachments.length }})</h4>
2020-02-01 22:21:22 +00:00
<attachment-display
v-for="item in foundAttachments"
:item="item"
:key="item.id"
:search-params="{}"
/>
</div>
</div>
<input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" ref="note1" />
<input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" ref="note2" />
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'SearchBar',
components: {
'input-notes': require('@/components/NoteInputPanel.vue').default,
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
'fast-filters': require('@/components/FastFilters.vue').default,
'search-input': require('@/components/SearchInput.vue').default,
2020-02-01 22:21:22 +00:00
'attachment-display': require('@/components/AttachmentDisplayCard').default,
'counter':require('@/components/AnimatedCounterComponent.vue').default
},
data () {
return {
initComponent: true,
commonTags: [],
searchTerm: '',
searchTags: [],
notes: [],
highlights: [],
searchDebounce: null,
fastFilters: {},
working: false,
//Load up notes in batches
firstLoadBatchSize: 30, //First set of rapidly loaded notes
batchSize: 100, //Size of batch loaded when user scrolls through current batch
batchOffset: 0, //Tracks the current batch that has been loaded
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
loadingInProgress: false,
fetchTags: false,
//Clear button is not visible
showClear: false,
initialPostData: null,
currentPostData: null,
containsNormalNotes: 0,
containsPinnednotes: 0,
containsTextResults: 0,
// containsTagResults: 0,
// containsAttachmentResults: 0,
//Currently open notes in app
activeNoteId1: null,
activeNoteId2: null,
//Position determines how note is Positioned
activeNote1Position: 0,
activeNote2Position: 0,
lastVisibilityState: null,
foundAttachments: [],
noteSections: {
'pinned': {},
'archived': {},
'recieved': {},
'sent':{},
'notes':{},
'textMatch':{}
}
2020-02-01 22:21:22 +00:00
}
},
beforeMount(){
this.$parent.loginGateway()
//Update totals for app
this.$store.dispatch('fetchAndUpdateUserTotals')
this.$bus.$on('close_active_note', ({position, noteId, modified}) => {
this.closeNote(position)
this.$store.dispatch('fetchAndUpdateUserTotals')
if(modified){
this.updateSingleNote(noteId)
}
})
this.$bus.$on('note_deleted', (noteId) => {
//Remove deleted note from set, its deleted
this.notes.forEach( (note, index) => {
if(note.id == noteId){
if(note.pinned == 1){
this.containsPinnednotes--
} else {
this.containsNormalNotes--
}
this.notes.splice(index, 1)
}
})
})
this.$bus.$on('update_fast_filters', newFilter => {
this.fastFilters = newFilter
//Fast filters always return all the results and tags
this.search(true, this.batchSize, false).then( () => {
return this.fetchUserTags()
})
})
//Event to update search from other areas
this.$bus.$on('update_search_term', sentInSearchTerm => {
this.searchTerm = sentInSearchTerm
this.search(true, this.batchSize)
.then( () => {
2020-02-01 22:21:22 +00:00
this.searchAttachments()
return this.fetchUserTags()
})
})
//New note button pushes open note event
this.$bus.$on('open_note', noteId => {
this.openNote(noteId)
})
//Reload page content
this.$bus.$on('note_reload', () => {
this.reset()
})
//Mount notes on load if note ID is set
if(this.$route.params && this.$route.params.id){
const id = this.$route.params.id
this.openNote(id)
}
window.addEventListener('scroll', this.onScroll)
//Close notes when back button is pressed
window.addEventListener('hashchange', this.hashChangeAction)
2020-02-01 22:21:22 +00:00
//update note on visibility change
document.addEventListener('visibilitychange', this.visibiltyChangeAction);
},
beforeDestroy(){
window.removeEventListener('scroll', this.onScroll)
window.removeEventListener('hashchange', this.hashChangeAction)
document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
2020-01-03 01:54:11 +00:00
//We want to remove event listeners, but something here is messing them up and preventing ALL event listeners from working
// this.$off() // Remove all event listeners
// this.$bus.$off()
},
mounted() {
//Loads initial batch and tags
this.reset()
},
methods: {
showOneColumn(){
//If note 1 or 2 is open, show one column. Or if the user is on mobile
return (this.activeNoteId1 != null || this.activeNoteId2 != null) &&
!this.$store.getters.getIsUserOnMobile
},
openNote(id, event = null){
//Don't open note if a link is clicked in display card
if(event && event.target && event.target.nodeName){
const nodeClick = event.target.nodeName
if(nodeClick == 'A'){ return }
}
//Do not open same note twice
if(this.activeNoteId1 == id || this.activeNoteId2 == id){
return;
}
//1 note open
if(this.activeNoteId1 == null && this.activeNoteId2 == null){
this.activeNoteId1 = id
this.activeNote1Position = 0 //Middel of page
this.$router.push('/notes/open/'+this.activeNoteId1)
return
}
//2 notes open
if(this.activeNoteId1 != null && this.activeNoteId2 == null){
this.activeNoteId2 = id
this.activeNote1Position = 1 //Right side of page
this.activeNote2Position = 2 //Left side of page
return
}
//2 notes open
if(this.activeNoteId2 != null && this.activeNoteId1 == null){
this.activeNoteId1 = id
this.activeNote1Position = 2 //Right side of page
this.activeNote2Position = 1 //Left side of page
return
}
},
closeNote(position){
//One note open, close that note
if(position == 0){
this.activeNoteId1 = null
this.activeNoteId2 = null
}
//Right note closed, thats 1
if(position == 1){
this.activeNoteId1 = null
}
if(position == 2){
this.activeNoteId2 = null
}
//IF two notes get opened, update ID of open note
if(this.activeNoteId1 || this.activeNoteId2){
this.$router.push('/notes/open/'+Math.max(this.activeNoteId1,this.activeNoteId2))
} else {
//No notes are open, just show notes page
this.$router.push('/notes')
}
this.activeNote1Position = 0
this.activeNote2Position = 0
},
toggleTagFilter(tagId){
if(this.searchTags.includes(tagId)){
this.searchTags.splice( this.searchTags.indexOf(tagId) , 1);
} else {
this.searchTags.push(tagId)
}
//Reset note set and load up notes and tags
if(this.searchTags.length > 0){
this.search(true, this.batchSize)
return
}
//If no tags are selected, reset entire page
this.reset()
},
onScroll(e){
clearTimeout(this.loadingBatchTimeout)
this.loadingBatchTimeout = setTimeout(() => {
//Distance to bottom of page
const bottomOfWindow =
Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop)
+ window.innerHeight
//height of page
const offsetHeight = this.$refs.content.clientHeight
//Determine percentage down the page
const percentageDown = Math.round( (bottomOfWindow/offsetHeight)*100 )
// console.log( percentageDown + '%' )
//If greater than 80 of the way down the page, load the next batch
if(percentageDown >= 80){
this.search(false, this.batchSize, true)
}
}, 50)
return
},
//Try to close notes on URL hash change /notes/open/123 to /notes - parse 123, close note id 123
hashChangeAction(event){
//Clean up path of hash change
let path = window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.hash
let newPath = event.newURL.replace(path,'')
let oldPath = event.oldURL.replace(path,'')
//If we go from open note ID to no note ID, close the note
if(newPath == '' && oldPath.indexOf('/open/') != -1){
//Pull note ID out of URL
const noteIdToClose = oldPath.split('/').pop()
if(this.$refs.note1 && this.$refs.note1.currentNoteId == noteIdToClose){
this.$refs.note1.close()
}
if(this.$refs.note2 && this.$refs.note2.currentNoteId == noteIdToClose){
this.$refs.note2.close()
}
}
},
visibiltyChangeAction(event){
//@TODO - set a timeout on this like 2 minutes or just dont do shit and update it via socket.io
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
// console.log('Welcome back. Reloading a batch')
//Load initial batch, then tags, then other batch
this.search(false, this.firstLoadBatchSize)
.then( () => {
return this.fetchUserTags()
})
}
this.lastVisibilityState = document.visibilityState
},
// @TODO Don't even trigger this if the note wasn't changed
updateSingleNote(noteId){
//Lookup one note using passed in ID
const postData = {
searchQuery: this.searchTerm,
searchTags: this.searchTags,
fastFilters:{
noteIdSet:[noteId]
}
}
axios.post('/api/note/search', postData)
.then(results => {
//Pull note data out of note set
let newNote = results.data.notes[0]
let foundNote = false
if(newNote === undefined){
console.log('Note not visible on this page')
return
}
//Go through each note and find the one just updated
this.notes.forEach( (note,index) => {
if(note.id == noteId){
foundNote = true
//Don't move notes that were not changed
if(note.updated == newNote.updated){
return
}
//Compare note tags, if they changed, reload tags
if(newNote.tag_count != note.tag_count){
console.log('Tags changed, update those bitches')
this.fetchUserTags()
}
//go through each prop and update it with new values
Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop]
})
this.notes.splice(index, 1)
this.notes.unshift(note)
}
})
//This note was not found, update it in list
if(foundNote == false){
if(newNote.pinned == 1){
this.containsPinnednotes++
} else {
this.containsNormalNotes++
}
this.notes.unshift(newNote)
}
})
},
2020-02-01 22:21:22 +00:00
searchAttachments(){
axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm})
.then(results => {
console.log('Attachment Results')
console.log(results.data)
this.foundAttachments = results.data
})
},
search(showLoading = true, notesInNextLoad = null, mergeExisting = false){
return new Promise((resolve, reject) => {
//Don't double load note batches
if(this.loadingInProgress){
return resolve()
}
//Reset a lot of stuff if we are not merging batches
if(!mergeExisting){
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
// this.commonTags = [] //Don't reset tags, if search returns tags, they will be set
}
//Remove all filter limits from previous queries
delete this.fastFilters.limitSize
delete this.fastFilters.limitOffset
let postData = {
searchQuery: this.searchTerm,
searchTags: this.searchTags,
fastFilters: this.fastFilters,
}
if(showLoading){
this.working = true
}
//Save initial post data on first load
if(this.initialPostData == null){
this.initialPostData = JSON.stringify(postData)
}
//If post data is not the same as initial, show clear button
if(JSON.stringify(postData) != this.initialPostData){
this.showClear = true
}
if(notesInNextLoad && notesInNextLoad > 0){
//Create limit based off of the number of notes already loaded
postData.fastFilters.limitSize = notesInNextLoad
postData.fastFilters.limitOffset = this.batchOffset
}
//Perform search - or die
this.loadingInProgress = true
axios.post('/api/note/search', postData).
then(response => {
//Save the number of notes just loaded
this.batchOffset += response.data.notes.length
//Mush the two new sets of data together (set will be empty is reset is on)
if(response.data.tags.length > 0){
this.commonTags = response.data.tags
}
//Either reload all notes with return data or merge return data
if(!mergeExisting){
this.notes = response.data.notes
} else {
this.notes = this.notes.concat(response.data.notes)
}
//Go through each note and see which section to display
let textResultsCount = 0
let pinnedResultsCount = 0
let normalNotesCount = 0
response.data.notes.forEach(note => {
if(note.note_highlights.length > 0){
textResultsCount++
return
}
if(note.pinned == 1){
pinnedResultsCount++
return
}
normalNotesCount++
})
if(!mergeExisting){
this.containsNormalNotes = normalNotesCount
this.containsPinnednotes = pinnedResultsCount
this.containsTextResults = textResultsCount
} else {
this.containsNormalNotes += normalNotesCount
this.containsPinnednotes += pinnedResultsCount
this.containsTextResults += textResultsCount
}
this.working = false
this.loadingInProgress = false
return resolve(true)
})
})
},
searchKeyUp(){
let vm = this
clearTimeout(vm.searchDebounce)
vm.searchDebounce = setTimeout(() => {
this.search(true, this.batchSize)
.then( () => {
return this.fetchUserTags()
})
}, 500)
},
ucWords(str){
return (str + '')
.replace(/^(.)|\s+(.)/g, function ($1) {
return $1.toUpperCase()
})
},
reset(){
this.showClear = false
this.searchTerm = ''
this.searchTags = []
this.fastFilters = {}
2020-02-01 22:21:22 +00:00
this.foundAttachments = [] //Remove all attachments
this.$bus.$emit('reset_fast_filters')
//Load initial batch, then tags, then other batch
this.search(true, this.firstLoadBatchSize)
.then( () => {
return this.fetchUserTags()
})
.then( () => {
//Load a larger batch once first batch has loaded
return this.search(false, this.batchSize, true)
})
.then( i => {
//Thats how you promise chain
})
},
fetchUserTags(){
return new Promise((resolve, reject) => {
let postData = {
searchQuery: this.searchTerm,
searchTags: this.searchTags,
fastFilters: this.fastFilters,
}
axios.post('/api/tag/usertags', postData)
.then( ({data}) => {
this.commonTags = data
resolve(data)
})
})
},
updateFastFilters(index){
//clear out tags
this.searchTags = []
//A little hacky, brings user to notes page then filters on click
if(this.$route.name != 'NotesPage'){
this.$router.push('/notes')
setTimeout( () => {
this.updateFastFilters(index)
}, 500 )
}
const options = [
'withLinks', // 'Only Show Notes with Links'
'withTags', // 'Only Show Notes with Tags'
'onlyArchived', //'Only Show Archived Notes'
'onlyShowSharedNotes', //Only show shared notes
]
let filter = {}
filter[options[index]] = 1
this.$bus.$emit('update_fast_filters', filter)
}
}
}
</script>
<style type="text/css" scoped>
.mush-it-up {
width: calc(50% - 130px);
}
.detail {
float: right;
}
.note-card-display-area {
display: flex;
flex-wrap: wrap;
}
.display-area-title {
width: 100%;
display: inline-block;
}
.note-card-section {
/*padding-bottom: 15px;*/
}
.note-card-section + .note-card-section {
padding: 15px 0 0;
}
</style>