I swear, I'm going to start doing regular commits

+ Added a ton of shit
+ About to add socket.io oh god.
This commit is contained in:
Max G 2020-01-03 01:26:55 +00:00
parent 6fe39406b7
commit abb4e20ec3
24 changed files with 3171 additions and 360 deletions

View File

@ -1,8 +1,7 @@
#!/bin/bash #!/bin/bash
BACKUPDIR="databaseBackupPi" BACKUPDIR="/home/mab/databaseBackupPi"
cd ..
mkdir -p $BACKUPDIR mkdir -p $BACKUPDIR
cd $BACKUPDIR cd $BACKUPDIR
@ -11,7 +10,7 @@ ssh mab@avidhabit.com -p 13328 "mysqldump --all-databases --user root -p***REMOV
cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/backup-$NOW.sql" cp "backup-$NOW.sql" "/mnt/Windows Data/DatabaseBackups/backup-$NOW.sql"
echo "Database Backup Complete" echo "Database Backup Complete on $NOW"
#Restore DB #Restore DB
# copy file over, run restore # copy file over, run restore

View File

@ -42,7 +42,7 @@ module.exports = {
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
}, },
{ {
test: /\.(png|jpe?g|gif)(\?.*)?$/, test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader', loader: 'url-loader',
options: { options: {
limit: 10000, limit: 10000,
@ -58,7 +58,7 @@ module.exports = {
} }
}, },
{ {
test: /\.(eot|ttf|otf|woff|woff2|svg)(\?.*)?$/, test: /\.(eot|ttf|otf|woff|woff2)(\?.*)?$/,
loader: 'url-loader', loader: 'url-loader',
options: { options: {
limit: 10000, limit: 10000,

View File

@ -97,12 +97,16 @@ div.ui.basic.green.label {
/* Styles for public display pages */ /* Styles for public display pages */
.note-status-indicator { .note-status-indicator {
float: right; position: absolute;
width: 100px; width: 100px;
padding: 9px 0; padding: 16px 0;
box-sizing: border-box; box-sizing: border-box;
text-align: center; text-align: center;
color: var(--text_color); color: var(--text_color);
bottom: 0;
right: 0;
z-index: 100;
cursor: pointer;
} }

View File

@ -1,110 +0,0 @@
<style scoped>
.shade {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0,0,0,0.7);
z-index: 1200;
cursor: pointer;
}
.editor {
position: fixed;
top: 10%;
bottom: 10%;
left: 10%;
right: 10%;
z-index: 1300;
overflow-y: scroll;
background-color: var(--background_color);
border: 3px solid;
border-color: var(--border_color);
}
.attachment-image {
width: 100%;
}
</style>
<template>
<div>
<div class="shade" v-on:click="closeAttachmentEditor"></div>
<div class="editor">
<div class="ui basic segment">
<h2>Edit Attachments</h2>
<div class="ui vertically divided grid">
<div v-for="item in attachments" class="row">
<div class="ui sixteen wide column">
<div class="ui form">
<div class="field">
<textarea v-on:blur="saveIt(item)" v-model="item.text" :key="item.id + 'text'"></textarea>
</div>
</div>
</div>
<div class="ui sixteen wide column">
<a v-if="item.url" :href="item.url" target="_blank">{{item.url}}</a>
<!-- Display image -->
<div v-if="item.file_location && item.attachment_type == 2">
<img class="attachment-image" :src="`/api/static/${item.file_location}`">
</div>
<!-- provide link -->
<a v-if="item.file_location" :href="`/api/static/${item.file_location}`" target="_blank">Download</a>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'AttachmentEditor',
props: [ 'noteId' ],
data: function(){
return {
attachments: []
}
},
beforeCreate: function(){
},
mounted: function(){
this.fetchAttachments()
},
methods: {
fetchAttachments(){
axios.post('/api/attachment/get', {'noteId':this.noteId})
.then(results => {
this.attachments = results.data
})
},
closeAttachmentEditor(){
this.$bus.$emit('close_edit_attachment')
},
saveIt(attachment){
const data = {
'attachmentId': attachment.id,
'updatedText': attachment.text,
'noteId': this.noteId
}
axios.post('/api/attachment/update', data)
.then(results => {})
},
}
}
</script>

View File

@ -7,9 +7,12 @@
</style> </style>
<template> <template>
<div class="ui clickable basic button"> <div class="ui small compact basic icon button clickable">
<form> <form>
<label :for="`upfile-${noteId}`" class="clickable">File Yeet {{uploadPercentage}}</label> <label :for="`upfile-${noteId}`" class="clickable">
<i class="upload icon"></i>
Upload File <span v-if="uploadPercentage != 0">{{uploadPercentage}}%</span>
</label>
<input class="hidden-up" type="file" :id="`upfile-${noteId}`" ref="file" v-on:change="handleFileUpload()" /> <input class="hidden-up" type="file" :id="`upfile-${noteId}`" ref="file" v-on:change="handleFileUpload()" />
</form> </form>
<!-- <button v-if="file" v-on:click="uploadFileToServer()">Submit</button> --> <!-- <button v-if="file" v-on:click="uploadFileToServer()">Submit</button> -->
@ -47,7 +50,7 @@
} }
} }
).then(results => { ).then(results => {
this.uploadPercentage = 'DONE' this.uploadPercentage = 0
this.file = null this.file = null
console.log('SUCCESS!!'); console.log('SUCCESS!!');
@ -76,7 +79,7 @@
}) })
.catch(results => { .catch(results => {
this.uploadPercentage = 'FAIL' this.uploadPercentage = 0
console.log('FAILURE!!'); console.log('FAILURE!!');
}) })
}, },

View File

@ -21,10 +21,10 @@
.menu-item { .menu-item {
color: #fff; color: #fff;
padding: 10px 0px 10px 10px; padding: 0.8em 0px 0.8em 0.8em;
display: inline-block; display: inline-block;
width: 100%; width: 100%;
font-size: 1.1rem; font-size: 1.15em;
box-sizing: border-box; box-sizing: border-box;
} }
.sub { .sub {
@ -60,28 +60,60 @@
cursor: pointer; cursor: pointer;
} }
.top-menu-bar { .top-menu-bar {
color: var(--text_color); /*color: var(--text_color);*/
width: calc(100% - 20px); /*width: 100%;*/
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background-color: var(--background_color);
border-bottom: 1px solid;
border-color: var(--border_color);
padding: 5px 1rem 5px;
}
.place-holder {
width: 100%;
height: 50px;
} }
.top-menu-bar img { .top-menu-bar img {
width: 20px; width: 30px;
height: 20px; height: 30px;
} }
</style> </style>
<template> <template>
<div> <div>
<div class="place-holder" v-if="collapsed && !menuOpen"></div>
<!-- collapsed menu, appears as a bar --> <!-- collapsed menu, appears as a bar -->
<div class="top-menu-bar menu-item" v-if="collapsed || mobile" v-on:click="collapseMenu"> <div class="top-menu-bar" v-if="(collapsed || mobile) && !menuOpen">
<div class="ui grid"> <div class="ui grid">
<div class="five wide column">
<i class="bars icon"></i> Menu <div class="three wide column">
<div class="ui large basic compact icon button" v-on:click="collapseMenu">
<i class="green bars icon"></i>
</div> </div>
<div class="six wide center aligned column">
<img src="/api/static/assets/favicon.ico" alt="logo" />
</div> </div>
<div class="five wide right aligned column"></div> <div class="ten wide center aligned column">
<img v-if="!loggedIn" src="/api/static/assets/favicon.ico" alt="logo" />
<search-input v-if="loggedIn"></search-input>
</div>
<div class="three wide right aligned column">
<!-- mobile create note button -->
<div v-if="loggedIn">
<div v-if="!disableNewNote" @click="createNote" class="ui large basic compact icon button">
<i class="green plus icon"></i>
</div>
<div v-if="disableNewNote" class="ui large basic compact icon button">
<i class="grey plus icon"></i>
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -94,7 +126,7 @@
<div class="menu-section"> <div class="menu-section">
<div class="menu-item menu-button" v-on:click="collapseMenu"> <div class="menu-item menu-button" v-on:click="collapseMenu">
<i class="caret square left icon"></i> <i class="caret square left outline icon"></i>
</div> </div>
</div> </div>
@ -108,7 +140,7 @@
</div> </div>
<div class="menu-section" v-if="loggedIn"> <div class="menu-section" v-if="loggedIn">
<router-link exact-active-class="active" class="menu-item menu-button" to="/notes"> <router-link exact-active-class="active" class="menu-item menu-button" to="/notes" v-on:click.native="emitReloadEvent()">
<i class="file icon"></i>Notes <i class="file icon"></i>Notes
</router-link> </router-link>
<div> <div>
@ -168,13 +200,16 @@
import axios from 'axios' import axios from 'axios'
export default { export default {
components: {
'search-input': require('@/components/SearchInput.vue').default,
},
data: function(){ data: function(){
return { return {
username: '', username: '',
collapsed: false, collapsed: false,
mobile: false, mobile: false,
disableNewNote: false disableNewNote: false,
menuOpen: true,
} }
}, },
beforeCreate: function(){ beforeCreate: function(){
@ -182,6 +217,10 @@
mounted: function(){ mounted: function(){
this.mobile = this.$store.getters.getIsUserOnMobile this.mobile = this.$store.getters.getIsUserOnMobile
this.collapsed = this.$store.getters.getIsUserOnMobile this.collapsed = this.$store.getters.getIsUserOnMobile
if(this.mobile){
this.menuOpen = false
}
}, },
computed: { computed: {
loggedIn () { loggedIn () {
@ -194,10 +233,18 @@
//Collapse menu when item is clicked in mobile //Collapse menu when item is clicked in mobile
if(this.mobile && !this.collapsed){ if(this.mobile && !this.collapsed){
this.collapsed = true this.collapsed = true
this.menuOpen = false
} }
}, },
collapseMenu(){ collapseMenu(){
this.collapsed = !this.collapsed this.collapsed = !this.collapsed
if(!this.collapsed){
this.menuOpen = true
} else {
this.menuOpen = false
}
}, },
createNote(event){ createNote(event){
const title = '' const title = ''
@ -227,6 +274,10 @@
return $1.toUpperCase() return $1.toUpperCase()
}) })
}, },
emitReloadEvent(){
//Reloads note page to initial state
this.$bus.$emit('note_reload')
},
updateFastFilters(index){ updateFastFilters(index){
//A little hacky, brings user to notes page then filters on click //A little hacky, brings user to notes page then filters on click

View File

@ -3,7 +3,7 @@
<span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete" data-inverted="" data-position="top right"> <span class="clickable" @click="confirmDelete()" v-if="click == 0" data-tooltip="Delete" data-inverted="" data-position="top right">
<i class="grey trash alternate icon"></i> <i class="grey trash alternate icon"></i>
</span> </span>
<span class="clickable" @click="actuallyDelete()" @mouseleave="reset" v-if="click == 1" data-tooltip="Click again to delete." data-position="left center" data-inverted=""> <span class="clickable" @click="actuallyDelete()" @mouseleave="reset" v-if="click == 1" data-tooltip="Click again to delete." data-position="top right" data-inverted="">
<i class="red trash alternate icon"></i> <i class="red trash alternate icon"></i>
</span> </span>
</span> </span>
@ -26,9 +26,11 @@
this.click++ this.click++
}, },
actuallyDelete(){ actuallyDelete(){
axios.post('/api/note/delete', {'noteId':this.noteId}).then(response => { axios.post('/api/note/delete', {'noteId':this.noteId}).then(response => {
if(response.data == true){ if(response.data == true){
this.$bus.$emit('note_deleted') console.log('Lets delete this note!')
this.$bus.$emit('note_deleted', this.noteId)
} }
}) })
}, },

View File

@ -15,39 +15,11 @@
</div> </div>
</div> </div>
<!-- Menu --> <span class="note-status-indicator" v-on:click="save()">{{statusText}}</span>
<div class="note-top-menu">
<div @click="close" class="ui basic icon button" data-tooltip="Close" data-position="right center" data-inverted="">
<i class="close icon"></i>
</div>
<div @click="onTogglePinned" class="ui basic icon button" data-tooltip="Pin to Top" data-position="right center" data-inverted="">
<i class="pin icon" :class="{green:(pinned == 1)}"></i> {{(pinned == 1)?'Pinned':''}}
</div>
<div @click="onToggleArchived" class="ui basic icon button" data-tooltip="Hide on main" data-position="right center" data-inverted="">
<i class="archive icon" :class="{green:(archived == 1)}"></i> {{(archived == 1)?'Archived':''}}
</div>
<!-- <file-upload-button :noteId="noteid" /> -->
<span class="relative" v-on:click="showColorPicker">
<span class="ui basic icon button">
<i class="paint brush icon"></i>
</span>
</span>
<div v-if="attachmentCount > 0" class="ui basic icon button" v-on:click="openEditAttachment" data-tooltip="View Links/Images" data-position="right center" data-inverted="">
<i class="folder icon"></i>
</div>
<span class="note-status-indicator">{{statusText}}</span>
</div>
<div class="tinymce-container">
<textarea :id="noteid+'-tinymce-editor'">{{noteText}}</textarea> <textarea :id="noteid+'-tinymce-editor'">{{noteText}}</textarea>
</div>
<note-tag-edit v-if="!$store.getters.getIsUserOnMobile" :noteId="noteid" :key="'tags-for-note-'+noteid"/>
<color-picker <color-picker
v-if="colorPickerVisible" v-if="colorPickerVisible"
@ -56,7 +28,51 @@
:style-object="styleObject" :style-object="styleObject"
/> />
<div v-if="$store.getters.getIsNoteSettingsOpen">
<div class="all-settings">
<div class="ui grid">
<div class="row">
<div class="sixteen wide column">
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/><br>
<!-- close button -->
<div class="ui small compact basic icon button" v-on:click="$store.commit('toggleNoteSettingsPane')">
<i class="close icon"></i> Close Settings
</div>
<!-- Pin Button -->
<div @click="onToggleArchived" class="ui small compact basic icon button">
<i class="archive icon" :class="{green:(archived == 1)}"></i>
{{(archived == 1)?'Archived':'Archive'}}
</div>
<!-- archive button -->
<div @click="onTogglePinned" class="ui small compact basic icon button">
<i class="pin icon" :class="{green:(pinned == 1)}"></i>
{{(pinned == 1)?'Pinned':'Pin'}}
</div>
<!-- colors button -->
<span class="relative" v-on:click="showColorPicker">
<span class="ui small compact basic icon button">
<i class="paint brush icon"></i>
Colors
</span>
</span>
<!-- attachment button -->
<div v-if="attachmentCount > 0" class="ui small compact basic icon button" v-on:click="openEditAttachment">
<i class="folder icon"></i> Attachments
</div>
<!-- file upload button -->
<file-upload-button :noteId="noteid" />
</div>
</div>
</div>
</div>
<!-- <div class="shade" v-on:click="showAllSettings = false"></div> -->
</div>
</div> </div>
</template> </template>
@ -72,6 +88,7 @@
'note-tag-edit': require('@/components/NoteTagEdit.vue').default, 'note-tag-edit': require('@/components/NoteTagEdit.vue').default,
'color-picker': require('@/components/ColorPicker.vue').default, 'color-picker': require('@/components/ColorPicker.vue').default,
'file-upload-button': require('@/components/FileUploadButton.vue').default, 'file-upload-button': require('@/components/FileUploadButton.vue').default,
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
}, },
data(){ data(){
return { return {
@ -95,6 +112,10 @@
colorPickerLocation: null, colorPickerLocation: null,
tinymce: null, //Initialized editor instance tinymce: null, //Initialized editor instance
//Settings vars
showAllSettings: true,
lastVisibilityState: null,
} }
}, },
watch: { watch: {
@ -114,10 +135,21 @@
} }
}, },
beforeMount(){ beforeMount(){
//Load tinymce into the page only do it once
if(document.querySelectorAll('[data-mceload]').length == 0){
let tinyMceIncluder = document.createElement('script')
tinyMceIncluder.setAttribute('src', '/api/static/assets/tinymce/tinymce.min.js')
tinyMceIncluder.setAttribute('data-mceload','loaded')
document.head.appendChild(tinyMceIncluder)
}
}, },
beforeDestroy(){ beforeDestroy(){
this.$bus.$off('toggle_night_mode', this.listener)
document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
this.$off() // Remove all event listeners
this.$bus.$off()
//Trash editor instance on close //Trash editor instance on close
this.tinymce.remove() this.tinymce.remove()
@ -127,6 +159,8 @@
//Change TinyMce styles on nightmored change //Change TinyMce styles on nightmored change
this.$bus.$on('toggle_night_mode', this.setEditorTextColor ) this.$bus.$on('toggle_night_mode', this.setEditorTextColor )
document.addEventListener('visibilitychange', this.checkForUpdatedNote)
this.$nextTick(() => { this.$nextTick(() => {
this.loadNote(this.noteid) this.loadNote(this.noteid)
}) })
@ -139,10 +173,17 @@
// {title: 'My image 2', value: 'http://www.moxiecode.com/my2.gif'} // {title: 'My image 2', value: 'http://www.moxiecode.com/my2.gif'}
// ] // ]
//Default plugin options for big browsers
let toolbarOptions = 'customCloseButton | mceTogglePinned | forecolor styleselect | bold italic underline | link | bullist numlist | outdent indent table | h | image'
let pluginOptions = 'paste, link, code, lists, table, hr, image'
//Tweak doc height for mobile //Tweak doc height for mobile
let docHeight = 'calc(100vh - 90px)' let docHeight = 'calc(100vh - 1px)'
if(this.$store.getters.getIsUserOnMobile){ if(this.$store.getters.getIsUserOnMobile){
docHeight = 'calc(100vh - 37px)' docHeight = 'calc(100vh - 1px)'
toolbarOptions = 'customCloseButton | bullist numlist | mceTogglePinned'
pluginOptions = 'lists'
} }
//setup skin as dark if night mode is enabled //setup skin as dark if night mode is enabled
@ -152,12 +193,13 @@
} }
const editorId = '#'+this.noteid+'-tinymce-editor' const editorId = '#'+this.noteid+'-tinymce-editor'
let vm = this
//Globally defined included in index HTML //Globally defined included in index HTML
tinymce.init({ tinymce.init({
selector: editorId, selector: editorId,
toolbar: 'forecolor backcolor styleselect | bold italic underline | link image | code | undo redo | bullist numlist | outdent indent table, hr, searchreplace | removeformat', toolbar: toolbarOptions,
plugins: 'paste, link, code, lists, table, hr, searchreplace, image', plugins: pluginOptions,
browser_spellcheck: true, browser_spellcheck: true,
menubar: false, menubar: false,
branding: false, branding: false,
@ -167,6 +209,25 @@
contextmenu: false, contextmenu: false,
init_instance_callback: this.editorInitCallback, init_instance_callback: this.editorInitCallback,
imagetools_toolbar: "imageoptions", imagetools_toolbar: "imageoptions",
setup: editor => {
//Add custom buttons to tinymce instance
editor.ui.registry.addButton('customCloseButton', {
text: 'Close',
icon: 'close',
onAction: function (_) {
vm.close()
}
})
//Add custom buttons to tinymce instance
editor.ui.registry.addButton('mceTogglePinned', {
text: 'Note Settings',
icon: 'settings',
onAction: function (_) {
vm.$store.commit('toggleNoteSettingsPane')
}
})
}
}) })
}, },
editorInitCallback(editor){ editorInitCallback(editor){
@ -197,7 +258,9 @@
} }
} }
},
setText(inText){
return this.tinymce.setContent(inText)
}, },
getText(){ getText(){
//Return text from tinyMce Editor //Return text from tinyMce Editor
@ -208,7 +271,6 @@
this.colorPickerLocation = {'x':event.clientX, 'y':event.clientY} this.colorPickerLocation = {'x':event.clientX, 'y':event.clientY}
}, },
openEditAttachment(){ openEditAttachment(){
// this.$bus.$emit('open_edit_attachment', this.currentNoteId)
this.$router.push('/attachments/note/'+this.currentNoteId) this.$router.push('/attachments/note/'+this.currentNoteId)
}, },
onTogglePinned(){ onTogglePinned(){
@ -300,11 +362,12 @@
save(){ save(){
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
//Clear other debounced events to prevent double calling of save //Clear other debounced events to prevent double calling of save
clearTimeout(this.editDebounce) // clearTimeout(this.editDebounce)
//Don't save note if its hash doesn't change //Don't save note if its hash doesn't change
const currentNoteText = this.getText() const currentNoteText = this.getText()
if( this.lastNoteHash == this.hashString( currentNoteText )){ if( this.lastNoteHash == this.hashString( currentNoteText )){
this.statusText = 'Saved'
return resolve(true) return resolve(true)
} }
@ -341,6 +404,34 @@
}) })
}, },
checkForUpdatedNote(){
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
console.log('Checking for changes is note data')
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){ hashString(text){
var hash = 0; var hash = 0;
@ -369,7 +460,7 @@
this.sizeDown = true this.sizeDown = true
//This timeout allows animation to play before closing //This timeout allows animation to play before closing
setTimeout(() => { setTimeout(() => {
this.$bus.$emit('close_active_note', this.position) this.$bus.$emit('close_active_note', {position: this.position, noteId: this.noteid})
return return
}, 300) }, 300)
}) })
@ -381,6 +472,28 @@
<style type="text/css" scoped> <style type="text/css" scoped>
/*Settings manager styles */
.all-settings {
position: absolute;
bottom: -5px;
right: 10px;
left: 10px;
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: 1em;
}
/*End Settings manager styles */
.tinymce-container {
/* Uncomment this to see the */
/*border-bottom: 2px solid green !important;*/
}
.note-top-menu { .note-top-menu {
width: 100%; width: 100%;
display: inline-block; display: inline-block;

View File

@ -1,8 +1,9 @@
<template> <template>
<div v-on:mouseover="fullTagEdit = true"> <div v-on:click="fullTagEdit = true">
<!-- simple string view --> <!-- simple string view -->
<div v-if="!fullTagEdit" class="ui basic segment"> <!-- class="ui basic segment" -->
<div v-if="!fullTagEdit">
<div class="simple-tag-display"> <div class="simple-tag-display">
<!-- Show Loading --> <!-- Show Loading -->
@ -10,11 +11,11 @@
<!-- Default count --> <!-- Default count -->
<span v-if="loaded"> <span v-if="loaded">
<i class="tags icon"></i> <b>{{tags.length}} Tags</b> <i class="tags icon"></i> <b>{{tags.length}} Tag<span v-if="tags.length > 1">s</span></b>
</span> </span>
<!-- No tags default text --> <!-- No tags default text -->
<span v-if="tags.length == 0 && loaded" class="ui small compact green button"> <span v-if="tags.length == 0 && loaded" class="ui small compact basic green button">
<i class="plus icon"></i> Add a tag <i class="plus icon"></i> Add a tag
</span> </span>
@ -28,6 +29,13 @@
<!-- hover over view --> <!-- hover over view -->
<div v-if="fullTagEdit" class="full-tag-area fade-in-fwd" v-on:mouseleave="fullTagEdit = false; clearSuggestions()"> <div v-if="fullTagEdit" class="full-tag-area fade-in-fwd" v-on:mouseleave="fullTagEdit = false; clearSuggestions()">
<!-- existing tags -->
<div class="delete-tag-display" v-if="tags.length > 0">
<div class="ui icon large label" v-for="tag in tags" :class="{ 'green':(newTagInput == tag.text) }">
{{ucWords(tag.text)}} <i class="delete icon" v-on:click="removeTag(tag.id)"></i>
</div>
</div>
<!-- tag input and suggestion popup --> <!-- tag input and suggestion popup -->
<div class="ui form"> <div class="ui form">
<input <input
@ -45,13 +53,6 @@
</div> </div>
</div> </div>
<!-- existing tags -->
<div class="delete-tag-display" v-if="tags.length > 0">
<div class="ui icon large label" v-for="tag in tags" :class="{ 'green':(newTagInput == tag.text) }">
{{ucWords(tag.text)}} <i class="delete icon" v-on:click="removeTag(tag.id)"></i>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -233,28 +234,27 @@
/* note tag edit area */ /* note tag edit area */
.full-tag-area { .full-tag-area {
position: absolute;
bottom: 0;
left: 0;
right: 0;
color: var(--text_color); color: var(--text_color);
background-color: var(--background_color); background-color: var(--background_color);
padding: 15px; /*padding: 15px;*/
border: 1px solid; /*border: 1px solid;*/
border-color: var(--border_color); border-color: var(--border_color);
} }
.full-tag-area .delete-tag-display { .full-tag-area .delete-tag-display {
margin-top: 15px; /*margin-top: 15px;*/
} }
.full-tag-area .ui.label { .full-tag-area .ui.label {
margin-bottom: 5px; margin-bottom: 5px;
} }
.simple-tag-display { .simple-tag-display {
width: 100%; width: calc(100% - 0px);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-height: 35px; max-height: 35px;
color: var(--text_color);
cursor: pointer;
} }
/* tag suggestion box styles */ /* tag suggestion box styles */
@ -269,8 +269,8 @@
height: 40px; height: 40px;
padding: 10px 15px; padding: 10px 15px;
cursor: pointer; cursor: pointer;
background: white; background-color: var(--background_color);
color: black; color: var(--text_color);
} }
.suggestion-item.active { .suggestion-item.active {
background: green; background: green;

View File

@ -11,33 +11,45 @@
<div class="ui grid max-height"> <div class="ui grid max-height">
<!-- Show title and snippet below it --> <!-- Show title and snippet below it -->
<div class="top aligned row" @click.stop="onClick(note.id)"> <div class="top aligned row" @click.self="onClick(note.id)">
<div class="sixteen wide column overflow-hidden" @click="e => onClick(note.id, e)">
<!-- Title display -->
<div v-if="note.title.length > 0"
data-test-id="title"
:class="{ 'big-text':(note.titleLength <= 100), 'small-text-title':(note.titleLength >= 100) }"
v-html="note.title"></div>
<!-- Sub text display -->
<div v-if="note.subtext.length > 0 && !isShowingSearchResults()"
data-test-id="subtext"
:class="{ 'big-text':(note.subtextLength <= 100 && note.titleLength <= 100), 'small-text':(note.subtextLength >= 100) }"
v-html="note.subtext"></div>
<div class="sixteen wide column overflow-hidden">
<h3 class="clickable">{{note.title}}</h3>
<p v-if="!isShowingSearchResults()" class="clickable">{{note.subtext}}</p>
<!-- Display highlights from solr results --> <!-- Display highlights from solr results -->
<div v-if="note.note_highlights.length > 0 && textResults" class="term-usage"> <div v-if="note.note_highlights.length > 0 && textResults" class="term-usage">
<div class="usage-row" v-for="highlight in note.note_highlights" v-html="cleanHighlight(highlight)"></div> <div
</div> class="usage-row"
v-for="highlight in note.note_highlights"
:class="{ 'big-text':(highlight <= 100), 'small-text-title':(highlight >= 100) }"
v-html="cleanHighlight(highlight)"></div>
</div> </div>
<!-- <div class="sixteen wide column overflow-hidden" v-if="isShowingSearchResults()"> </div>
</div> -->
</div> </div>
<!-- Toolbar on the bottom --> <!-- Toolbar on the bottom -->
<div class="bottom aligned row icon-bar" @click.self.stop="onClick(note.id)"> <div class="bottom aligned row icon-bar" @click.self="onClick(note.id)">
<div class="six wide column clickable" @click.stop="onClick(note.id)"> <div class="six wide column clickable" @click="onClick(note.id)">
{{$helpers.timeAgo(note.updated)}} {{$helpers.timeAgo(note.updated)}}
<!-- {{(note.chars.toLocaleString())}} -->
</div> </div>
<div class="ten wide right aligned column split-spans"> <div class="ten wide right aligned column">
<delete-button class="hover-hide" :note-id="note.id" /> <!-- ALways show delete button on mobile -->
<delete-button :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }" :note-id="note.id" />
<span v-if="note.pinned == 1" data-position="top right" data-tooltip="Pinned" data-inverted=""> <span v-if="note.pinned == 1" data-position="top right" data-tooltip="Pinned" data-inverted="">
<i class="green pin icon"></i> <i class="green pin icon"></i>
@ -45,7 +57,7 @@
<span v-if="note.archived == 1" data-position="top right" data-tooltip="Archived" data-inverted=""> <span v-if="note.archived == 1" data-position="top right" data-tooltip="Archived" data-inverted="">
<i class="green archive icon"></i> <i class="green archive icon"></i>
</span> </span>
<span v-if="note.attachment_count > 0" v-on:click.stop="openEditAttachment"> <span v-if="note.attachment_count > 0" v-on:click.stop="openEditAttachment" class="clickable">
<i class="linkify icon"></i> {{note.attachment_count}} <i class="linkify icon"></i> {{note.attachment_count}}
</span> </span>
<span v-if="note.tag_count == 1" data-position="top right" data-tooltip="Note has 1 tag" data-inverted=""> <span v-if="note.tag_count == 1" data-position="top right" data-tooltip="Note has 1 tag" data-inverted="">
@ -88,7 +100,6 @@
}, },
openEditAttachment(){ openEditAttachment(){
this.$router.push('/attachments/note/'+this.note.id) this.$router.push('/attachments/note/'+this.note.id)
// this.$bus.$emit('open_edit_attachment', this.note.id)
}, },
}, },
data () { data () {
@ -132,6 +143,20 @@
</script> </script>
<style type="text/css"> <style type="text/css">
/*Strict font sizes for card display*/
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
font-size: 1.0em !important;
}
.small-text > p, , .small-text > h1, .small-text > h2 {
margin-bottom: 0.5em;
}
.big-text, .big-text > p, .big-text > h1, .big-text > h2 {
font-size: 1.3em !important;
}
.big-text > p, .big-text > h1, .big-text > h2 {
margin-bottom: 0.3em;
}
.note-title-display-card h3 { .note-title-display-card h3 {
font-size: 1rem; font-size: 1rem;
font-weight: bold; font-weight: bold;
@ -166,7 +191,7 @@
border: 1px solid; border: 1px solid;
border-color: var(--border_color); border-color: var(--border_color);
width: calc(33.333% - 10px); width: calc(33.333% - 10px);
transition: box-shadow 0.3s; /*transition: box-shadow 0.3s;*/
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;

View File

@ -0,0 +1,44 @@
<template>
<div class="ui form">
<div class="fields">
<div class="sixteen wide field">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" />
</div>
</div>
</div>
</template>
<script>
export default {
data: function(){
return {
searchTerm: '',
searchTimeout: null,
searchDebounceDuration: 300,
}
},
beforeCreate: function(){
},
mounted: function(){
//search clear
this.$bus.$on('reset_fast_filters', () => {
this.searchTerm = ''
})
},
methods: {
searchKeyUp(){
clearTimeout(this.searchTimeout)
this.searchTimeout = setTimeout(() => {
this.search()
}, this.searchDebounceDuration)
},
search(){
this.$bus.$emit('update_search_term', this.searchTerm)
},
}
}
</script>

View File

@ -32,7 +32,6 @@ require('./assets/themes/default/assets/fonts/outline-icons.woff2')
// This callback runs before every route change, including on page load. // This callback runs before every route change, including on page load.
// Sets the title of the page using vue router // Sets the title of the page using vue router
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {

View File

@ -62,6 +62,12 @@
100%{background-position:0% 50%} 100%{background-position:0% 50%}
} }
/*safari fix - prevents page from being below the menu */
.dont-pad-me {
margin-right: 0 !important;
margin-left: 0 !important;
}
</style> </style>
<template> <template>

View File

@ -4,15 +4,14 @@
<div class="ui grid" :class="{ 'mush-it-up':showOneColumn() }" ref="content"> <div class="ui grid" :class="{ 'mush-it-up':showOneColumn() }" ref="content">
<!-- Note filter options --> <!-- Note filter options -->
<div class="row"> <div class="row" v-if="!$store.getters.getIsUserOnMobile">
<div <div :class="{ 'sixteen wide column':showOneColumn(), 'eight wide column':!showOneColumn() }">
:class="{ 'sixteen wide column':showOneColumn(), 'eight wide column':!showOneColumn() }"
>
<div class="ui form"> <div class="ui form">
<div class="fields"> <div class="fields">
<div class="ten wide field"> <div class="ten wide field">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" /> <search-input></search-input>
<!-- <input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" /> -->
</div> </div>
<div class="six wide field"> <div class="six wide field">
<span class="ui fluid green button" <span class="ui fluid green button"
@ -20,16 +19,19 @@
@click="reset"> @click="reset">
<i class="undo icon"></i>Reset Filters <i class="undo icon"></i>Reset Filters
</span> </span>
<!-- <fast-filters /> -->
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div </div>
:class="{ 'sixteen wide column':showOneColumn(), 'eight wide column':!showOneColumn() }"
> <div v-if="$store.getters.getIsUserOnMobile && showClear" class="row">
<h2 class="ui right floated"> <div class="sixteen wide column">
<fast-filters /> <span class="ui fluid green button"
</h2> @click="reset">
<i class="undo icon"></i>Reset Filters
</span>
</div> </div>
</div> </div>
@ -64,7 +66,8 @@
<!-- pinned notes --> <!-- pinned notes -->
<div v-if="containsPinnednotes > 0" class="note-card-section"> <div v-if="containsPinnednotes > 0" class="note-card-section">
<h4><i class="green pin icon"></i>Pinned <span v-if="!working">({{containsPinnednotes}})</span></h4> <!-- ({{containsPinnednotes}}) -->
<h4><i class="green pin icon"></i>Pinned <span v-if="!working"></span></h4>
<div class="note-card-display-area"> <div class="note-card-display-area">
<note-title-display-card <note-title-display-card
v-for="note in notes" v-for="note in notes"
@ -79,7 +82,8 @@
<!-- normal notes --> <!-- normal notes -->
<div v-if="containsNormalNotes > 0" class="note-card-section"> <div v-if="containsNormalNotes > 0" class="note-card-section">
<h4>Notes <span v-if="!working">({{containsNormalNotes}})</span></h4> <!-- ({{containsNormalNotes}}) -->
<h4><i class="green file icon"></i>Notes <span v-if="!working"></span></h4>
<div class="note-card-display-area"> <div class="note-card-display-area">
<note-title-display-card <note-title-display-card
v-for="note in notes" v-for="note in notes"
@ -114,12 +118,8 @@
</div> </div>
<input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" /> <input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" ref="note1" />
<input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" /> <input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" ref="note2" />
<div v-if="openAttachmentEdit">
<edit-attachment :note-id="editAttchmentId" :key="editAttchmentId" />
</div>
</div> </div>
</template> </template>
@ -134,7 +134,7 @@
'input-notes': require('@/components/NoteInputPanel.vue').default, 'input-notes': require('@/components/NoteInputPanel.vue').default,
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default, 'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
'fast-filters': require('@/components/FastFilters.vue').default, 'fast-filters': require('@/components/FastFilters.vue').default,
'edit-attachment': require('@/components/AttachmentEditor.vue').default, 'search-input': require('@/components/SearchInput.vue').default,
}, },
data () { data () {
return { return {
@ -153,6 +153,7 @@
batchOffset: 0, //Tracks the current batch that has been loaded batchOffset: 0, //Tracks the current batch that has been loaded
loadingBatchTimeout: null, //Limit how quickly batches can be loaded loadingBatchTimeout: null, //Limit how quickly batches can be loaded
loadingInProgress: false, loadingInProgress: false,
fetchTags: false,
//Clear button is not visible //Clear button is not visible
showClear: false, showClear: false,
@ -162,8 +163,8 @@
containsNormalNotes: 0, containsNormalNotes: 0,
containsPinnednotes: 0, containsPinnednotes: 0,
containsTextResults: 0, containsTextResults: 0,
containsTagResults: 0, // containsTagResults: 0,
containsAttachmentResults: 0, // containsAttachmentResults: 0,
//Currently open notes in app //Currently open notes in app
activeNoteId1: null, activeNoteId1: null,
@ -173,47 +174,14 @@
activeNote1Position: 0, activeNote1Position: 0,
activeNote2Position: 0, activeNote2Position: 0,
//Attchment to edit. Only 1 for now lastVisibilityState: null,
openAttachmentEdit: false,
editAttchmentId: null,
} }
}, },
beforeMount(){ beforeMount(){
this.$parent.loginGateway() this.$parent.loginGateway()
this.$bus.$on('open_edit_attachment', noteId => {
this.openAttachmentEdit = true
this.editAttchmentId = noteId
})
this.$bus.$on('close_edit_attachment', noteId => {
this.openAttachmentEdit = false
this.editAttchmentId = null
})
this.$bus.$on('close_active_note', position => {
this.closeNote(position)
})
this.$bus.$on('note_deleted', () => {
this.search()
})
this.$bus.$on('update_fast_filters', newFilter => {
this.fastFilters = newFilter
this.search(true, this.batchSize, false)
})
//New note button pushes open note event
this.$bus.$on('open_note', noteId => {
this.openNote(noteId)
})
//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)
//Load tinymce into the page only do it once //Load tinymce into the page only do it once
if(document.querySelectorAll('[data-mceload]').length == 0){ if(document.querySelectorAll('[data-mceload]').length == 0){
let tinyMceIncluder = document.createElement('script') let tinyMceIncluder = document.createElement('script')
@ -222,21 +190,75 @@
document.head.appendChild(tinyMceIncluder) document.head.appendChild(tinyMceIncluder)
} }
this.$bus.$on('close_active_note', ({position, noteId}) => {
this.closeNote(position)
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( () => {
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)
document.addEventListener('visibilitychange', this.visibiltyChangeAction);
}, },
beforeDestroy(){ beforeDestroy(){
console.log('Unbinging all events')
window.removeEventListener('scroll', this.onScroll) window.removeEventListener('scroll', this.onScroll)
window.removeEventListener('hashchange', this.hashChangeAction)
document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
this.$off() // Remove all event listeners
this.$bus.$off()
}, },
mounted() { mounted() {
//Loads initial batch and tags
//Load a super fast batch this.reset()
this.search(true, this.firstLoadBatchSize, 0)
.then( () => {
//Load a larger batch once first batch has loaded
this.search(false, this.batchSize, true).then( () => {
})
})
}, },
methods: { methods: {
showOneColumn(){ showOneColumn(){
@ -244,7 +266,13 @@
return (this.activeNoteId1 != null || this.activeNoteId2 != null) && return (this.activeNoteId1 != null || this.activeNoteId2 != null) &&
!this.$store.getters.getIsUserOnMobile !this.$store.getters.getIsUserOnMobile
}, },
openNote(id){ 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 //Do not open same note twice
if(this.activeNoteId1 == id || this.activeNoteId2 == id){ if(this.activeNoteId1 == id || this.activeNoteId2 == id){
@ -287,12 +315,17 @@
this.activeNoteId2 = null 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.$router.push('/notes')
}
this.activeNote1Position = 0 this.activeNote1Position = 0
this.activeNote2Position = 0 this.activeNote2Position = 0
this.search(false)
}, },
toggleTagFilter(tagId){ toggleTagFilter(tagId){
@ -302,7 +335,15 @@
this.searchTags.push(tagId) this.searchTags.push(tagId)
} }
this.search() //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){ onScroll(e){
@ -325,7 +366,7 @@
//If greater than 80 of the way down the page, load the next batch //If greater than 80 of the way down the page, load the next batch
if(percentageDown >= 80){ if(percentageDown >= 80){
console.log('loading batch') console.log('loading next batch')
this.search(false, this.batchSize, true) this.search(false, this.batchSize, true)
} }
@ -334,14 +375,120 @@
return 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)
}
})
},
search(showLoading = true, notesInNextLoad = null, mergeExisting = false){ search(showLoading = true, notesInNextLoad = null, mergeExisting = false){
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
//Don't double load note batches
if(this.loadingInProgress){ if(this.loadingInProgress){
return resolve() return resolve()
} }
//Remove all filter limits //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.limitSize
delete this.fastFilters.limitOffset delete this.fastFilters.limitOffset
@ -375,40 +522,50 @@
axios.post('/api/note/search', postData). axios.post('/api/note/search', postData).
then(response => { then(response => {
if(!mergeExisting){
this.containsNormalNotes = 0
this.containsPinnednotes = 0
this.containsTextResults = 0
this.batchOffset = 0 // Reset batch offset if we are not merging note batches
this.commonTags = []
this.notes = []
}
//Save the number of notes just loaded //Save the number of notes just loaded
this.batchOffset += response.data.notes.length this.batchOffset += response.data.notes.length
//Mush the two new sets of data together //Mush the two new sets of data together (set will be empty is reset is on)
this.commonTags = this.commonTags.concat(response.data.tags) if(response.data.tags.length > 0){
this.notes = this.notes.concat(response.data.notes) 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 //Go through each note and see which section to display
let textResultsCount = 0
let pinnedResultsCount = 0
let normalNotesCount = 0
response.data.notes.forEach(note => { response.data.notes.forEach(note => {
if(note.note_highlights.length > 0){ if(note.note_highlights.length > 0){
this.containsTextResults++ textResultsCount++
return return
} }
if(note.pinned == 1){ if(note.pinned == 1){
this.containsPinnednotes++ pinnedResultsCount++
return return
} }
this.containsNormalNotes++ 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.working = false
this.loadingInProgress = false this.loadingInProgress = false
@ -420,7 +577,10 @@
let vm = this let vm = this
clearTimeout(vm.searchDebounce) clearTimeout(vm.searchDebounce)
vm.searchDebounce = setTimeout(() => { vm.searchDebounce = setTimeout(() => {
vm.search() this.search(true, this.batchSize)
.then( () => {
return this.fetchUserTags()
})
}, 500) }, 500)
}, },
ucWords(str){ ucWords(str){
@ -435,7 +595,28 @@
this.searchTags = [] this.searchTags = []
this.fastFilters = {} this.fastFilters = {}
this.$bus.$emit('reset_fast_filters') this.$bus.$emit('reset_fast_filters')
this.search(true, this.firstLoadBatchSize, 0)
//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) => {
axios.post('/api/tag/usertags')
.then( ({data}) => {
this.commonTags = data
resolve(data)
})
})
} }
} }
} }

View File

@ -11,8 +11,11 @@ const LoginPage = () => import('@/pages/LoginPage')
// import HelpPage from '@/pages/HelpPage' // import HelpPage from '@/pages/HelpPage'
const HelpPage = () => import('@/pages/HelpPage') const HelpPage = () => import('@/pages/HelpPage')
// import SharePage from '@/pages/SharePage'
const SharePage = () => import('@/pages/SharePage')
//These guys can all be loaded as a chunk
import NotesPage from '@/pages/NotesPage' import NotesPage from '@/pages/NotesPage'
import SharePage from '@/pages/SharePage'
import QuickPage from '@/pages/QuickPage' import QuickPage from '@/pages/QuickPage'
import AttachmentsPage from '@/pages/AttachmentsPage' import AttachmentsPage from '@/pages/AttachmentsPage'

View File

@ -10,6 +10,7 @@ export default new Vuex.Store({
username: null, username: null,
nightMode: false, nightMode: false,
isUserOnMobile: false, isUserOnMobile: false,
isNoteSettingsOpen: false, //Little note settings pane
}, },
mutations: { mutations: {
setLoginToken(state, userData){ setLoginToken(state, userData){
@ -77,7 +78,9 @@ export default new Vuex.Store({
state.isUserOnMobile = true state.isUserOnMobile = true
} }
})(navigator.userAgent||navigator.vendor||window.opera, state); })(navigator.userAgent||navigator.vendor||window.opera, state);
},
toggleNoteSettingsPane(state){
state.isNoteSettingsOpen = !state.isNoteSettingsOpen
} }
}, },
getters: { getters: {
@ -96,6 +99,9 @@ export default new Vuex.Store({
}, },
getIsUserOnMobile: state => { getIsUserOnMobile: state => {
return state.isUserOnMobile return state.isUserOnMobile
} },
getIsNoteSettingsOpen: state => {
return state.isNoteSettingsOpen
},
} }
}) })

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,140 @@
let ProcessText = module.exports = {} let ProcessText = module.exports = {}
ProcessText.removeHtml = (string) => { ProcessText.removeHtml = (string) => {
if(string == undefined || string == null || string.length == 0){
return ''
}
return string return string
.replace(/&[#A-Za-z0-9]+;/g,' ') //Rip out all HTML entities .replace(/&[#A-Za-z0-9]+;/g,' ') //Rip out all HTML entities
.replace(/<[^>]+>/g, ' ') //Rip out all HTML tags .replace(/<[^>]+>/g, ' ') //Rip out all HTML tags
.replace(/\s+/g, ' ') //Remove all whitespace .replace(/\s+/g, ' ') //Remove all whitespace
.trim()
}
ProcessText.getUrlsFromString = (string) => {
const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm
return string.match(urlPattern)
}
/*
Pulls out title and subtext of note
+ Title is always first line
+ Empty lines are skipped
+ URLs are turned into links
+ All URLs are givent the target="_blank" property
*/
ProcessText.deduceNoteTitle = (inString) => {
let title = '' //Title of note
let sub = '' //sub text below note
if(!inString || inString == null || inString.length == 0){
return {title, sub}
}
let lines = inString.match(/[^\r\n]+/g)
let finalLines = []
const startTags = ['<ol','<li','<ul']
const endTags = ['</o','</l','</u']
let totalLines = Math.min(lines.length, 6)
let charLimit = 250
let listStart = false
for(let i=0; i < totalLines; i++){
//Just in case 'i' gets bigger than array
if(lines[i] === undefined){
continue
}
const cleanLine = ProcessText.removeHtml(lines[i]).trim().replace('&nbsp','')
const lineStart = lines[i].trim().substring(0, 3)
charLimit -= cleanLine.length
//Close out list if char limit is hit
if(charLimit <= 0 && listStart){
finalLines.push(lines[i])
break
}
//Images appear as empty, push em!
if(cleanLine.length == 0 && lines[i].indexOf('<img') != -1){
finalLines.push(lines[i])
continue
}
//Empty line, may be a list open or close
if(cleanLine.length == 0 && (startTags.includes(lineStart) || endTags.includes(lineStart) )){
if(listStart == false){
charLimit = 400 //Double size for list notes
}
finalLines.push(lines[i])
totalLines++
listStart = true
continue
}
//If line is part of a list, up counter, we want the whole list
if(startTags.includes(lineStart)){
totalLines++
}
//Skip empty lines
if(!cleanLine || cleanLine.length == 0 || cleanLine == '&nbsp;'){
totalLines++
continue
}
//turn urls into links, don't process if its already an <a href=
const containsUrls = ProcessText.getUrlsFromString(cleanLine)
if(containsUrls && containsUrls.length == 1 && lines[i].indexOf('</a>') == -1){
const url = containsUrls[0]
lines[i] = lines[i].replace(url, `<a href="${url}">${url}</a>`)
}
//Insert target=_blank into links if set, do it for every link in line
if(lines[i].indexOf('</a>') > 0){
lines[i] = lines[i].replace(/<a /g, '<a target="_blank" ')
}
//Limit output characters
//Check character limit
if(charLimit <= 0 && listStart == false){
//Cut the string down to character limit
const cutString = lines[i].substring(0, lines[i].length+charLimit)
//Find last space and cut off everything after it
let cleanCutString = cutString.substring(0, cutString.lastIndexOf(' '))
//Some strings may not contain a space resulting in no string
if(cleanCutString.length == 0){
cleanCutString = cutString
}
finalLines.push(cleanCutString + '...')
break;
}
finalLines.push(lines[i])
}
//Pull out title if its not an empty string
if(ProcessText.removeHtml(finalLines[0]).trim().replace('&nbsp','').length > 0){
title = finalLines.shift()
}
sub = finalLines.join('')
//Return final display lengths
let titleLength = ProcessText.removeHtml(title).trim().replace('&nbsp','').length
let subtextLength = ProcessText.removeHtml(sub).trim().replace('&nbsp','').length
return { title, sub, titleLength, subtextLength }
} }

View File

@ -117,6 +117,7 @@ Attachment.scanTextForWebsites = (userId, noteId, noteText) => {
Attachment.urlForNote(userId, noteId).then(attachments => { Attachment.urlForNote(userId, noteId).then(attachments => {
//Find all URLs in text //Find all URLs in text
//@TODO - Use the process text library for this function
const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm const urlPattern = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$])/igm
let allUrls = noteText.match(urlPattern) let allUrls = noteText.match(urlPattern)

View File

@ -5,7 +5,10 @@ let Attachment = require('@models/Attachment')
let ProcessText = require('@helpers/ProcessText') let ProcessText = require('@helpers/ProcessText')
const DiffMatchPatch = require('@helpers/DiffMatchPatch')
var rp = require('request-promise'); var rp = require('request-promise');
const fs = require('fs')
let Note = module.exports = {} let Note = module.exports = {}
@ -137,19 +140,94 @@ Note.update = (userId, noteId, noteText, color, pinned, archived) => {
}) })
} }
//
// Delete a note and all its remaining parts
//
Note.delete = (userId, noteId) => { Note.delete = (userId, noteId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise().query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId,userId]) db.promise().query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId,userId])
.then((rows, fields) => { .then((rows, fields) => {
db.promise().query('DELETE FROM attachment WHERE attachment.note_id = ? AND attachment.user_id = ?', [noteId,userId]) return db.promise().query('DELETE FROM note_text_index WHERE note_text_index.note_id = ? AND note_text_index.user_id = ?', [noteId,userId])
})
.then((rows, fields) => { .then((rows, fields) => {
db.promise().query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId]) //Select all attachments with files
return db.promise().query('SELECT file_location FROM attachment WHERE attachment.note_id = ? AND attachment.user_id = ?', [noteId,userId])
})
.then((attachmentRows, fields) => {
//Go through each selected attachment and delete the files
attachmentRows[0].forEach( location => {
const fileName = location['file_location']
if(fileName != null && fileName.length > 1){
fs.unlink('../staticFiles/'+fileName ,function(err){ //Async, just rip through them.
if(err) return console.log(err);
// console.log('file deleted successfully => ', fileName);
})
}
})
return db.promise().query('DELETE FROM attachment WHERE attachment.note_id = ? AND attachment.user_id = ?', [noteId,userId])
})
.then((rows, fields) => {
return db.promise().query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
})
.then((rows, fields) => { .then((rows, fields) => {
resolve(true) resolve(true)
}) })
}) })
}
//text is the current text for the note that will be compared to the text in the database
Note.getDiffText = (userId, noteId, usersCurrentText, lastUpdated) => {
return new Promise((resolve, reject) => {
Note.get(userId, noteId)
.then(noteObject => {
let oldText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
let newText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
if(noteObject.updated == lastUpdated){
console.log('No note diff')
resolve(null)
}
if(noteObject.updated > lastUpdated){
newText = noteObject.text.replace(/(\r\n|\n|\r)/gm,"")
oldText = usersCurrentText.replace(/(\r\n|\n|\r)/gm,"")
}
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);
//Patch text - shows a list of changes
var patches = dmp.patch_fromText(patch_text);
// console.log(patch_text)
//results[1] - contains diagnostic data for patch apply, its possible it can fail
var results = dmp.patch_apply(patches, oldText);
//Compile return data for front end
const returnData = {
updatedText: results[0],
diffs: results[1].length, //Only use length for now
updated: Math.max(noteObject.updated,lastUpdated) //Return most recently updated date
}
//Final change in notes
console.log(returnData)
resolve(returnData)
}) })
}) })
} }
Note.get = (userId, noteId) => { Note.get = (userId, noteId) => {
@ -184,6 +262,7 @@ Note.getShared = (noteId) => {
}) })
} }
// Searches text index, returns nothing if there is no search query
Note.solrQuery = (userId, searchQuery, searchTags) => { Note.solrQuery = (userId, searchQuery, searchTags) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -243,27 +322,28 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
Note.solrQuery(userId, searchQuery, searchTags).then( (textSearchResults) => { Note.solrQuery(userId, searchQuery, searchTags).then( (textSearchResults) => {
//Pull out search results from previous query
let textSearchIds = [] let textSearchIds = []
let highlights = {} let highlights = {}
let returnTagResults = false
if(textSearchResults != null){ if(textSearchResults != null){
textSearchIds = textSearchResults['ids'] textSearchIds = textSearchResults['ids']
highlights = textSearchResults['snippets'] highlights = textSearchResults['snippets']
} }
//No results, return empty data //No results, return empty data
if(textSearchIds.length == 0 && searchQuery.length > 0){ if(textSearchIds.length == 0 && searchQuery.length > 0){
return resolve(returnData) return resolve(returnData)
} }
//Default note lookup gets all notes // Base of the query, modified with fastFilters
// Add to query for character counts -> CHAR_LENGTH(note.text) as chars // Add to query for character counts -> CHAR_LENGTH(note.text) as chars
let noteSearchQuery = ` let noteSearchQuery = `
SELECT note.id, SELECT note.id,
SUBSTRING(note.text, 1, 400) as text, SUBSTRING(note.text, 1, 1500) as text,
updated, color, updated,
color,
count(distinct note_tag.id) as tag_count, count(distinct note_tag.id) as tag_count,
count(distinct attachment.id) as attachment_count, count(distinct attachment.id) as attachment_count,
note.pinned, note.pinned,
@ -274,22 +354,26 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
WHERE note.user_id = ?` WHERE note.user_id = ?`
let searchParams = [userId] let searchParams = [userId]
//If text search returned results, limit search to those ids
if(textSearchIds.length > 0){ if(textSearchIds.length > 0){
searchParams.push(textSearchIds) searchParams.push(textSearchIds)
noteSearchQuery += ' AND note.id IN (?)' noteSearchQuery += ' AND note.id IN (?)'
} }
if(searchTags.length > 0){ if(fastFilters.noteIdSet && fastFilters.noteIdSet.length > 0){
searchParams.push(fastFilters.noteIdSet)
noteSearchQuery += ' AND note.id IN (?)'
}
//If tags are passed, use those tags in search //If tags are passed, use those tags in search
if(searchTags.length > 0){
searchParams.push(searchTags) searchParams.push(searchTags)
noteSearchQuery += ' AND note_tag.tag_id IN (?)' noteSearchQuery += ' AND note_tag.tag_id IN (?)'
} }
//Toggle archived, show archived if tags are searched //Show archived notes, only if fast filter is set, default to not archived
// - archived will show archived in search results if(fastFilters.onlyArchived == 1){
// - onlyArchive will exclude notes that are not archived noteSearchQuery += ' AND note.archived = 1' //Show Archived
if(fastFilters.archived == 1 || searchTags.length > 0 || fastFilters.onlyArchived == 1){
//Do nothing
} else { } else {
noteSearchQuery += ' AND note.archived = 0' //Exclude archived noteSearchQuery += ' AND note.archived = 0' //Exclude archived
} }
@ -299,14 +383,17 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
//Only show notes with Tags //Only show notes with Tags
if(fastFilters.withTags == 1){ if(fastFilters.withTags == 1){
returnTagResults = true
noteSearchQuery += ' HAVING tag_count > 0' noteSearchQuery += ' HAVING tag_count > 0'
} }
//Only show notes with links //Only show notes with links
if(fastFilters.withLinks == 1){ if(fastFilters.withLinks == 1){
returnTagResults = true
noteSearchQuery += ' HAVING attachment_count > 0' noteSearchQuery += ' HAVING attachment_count > 0'
} }
//Only show archived notes //Only show archived notes
if(fastFilters.onlyArchived == 1){ if(fastFilters.onlyArchived == 1){
returnTagResults = true
noteSearchQuery += ' HAVING note.archived = 1' noteSearchQuery += ' HAVING note.archived = 1'
} }
@ -336,10 +423,13 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
const limitOffset = parseInt(fastFilters.limitOffset, 10) || 0 //Either parse int, or use zero const limitOffset = parseInt(fastFilters.limitOffset, 10) || 0 //Either parse int, or use zero
console.log(` LIMIT ${limitOffset}, ${limitSize}`) // console.log(` LIMIT ${limitOffset}, ${limitSize}`)
noteSearchQuery += ` LIMIT ${limitOffset}, ${limitSize}` noteSearchQuery += ` LIMIT ${limitOffset}, ${limitSize}`
} }
// console.log('------------- Final Query --------------')
// console.log(noteSearchQuery)
// console.log('------------- ----------- --------------')
db.promise() db.promise()
.query(noteSearchQuery, searchParams) .query(noteSearchQuery, searchParams)
@ -357,37 +447,26 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
if(note.text == null){ note.text = '' } if(note.text == null){ note.text = '' }
//Attempt to pull string out of first tag in note //Deduce note title
let reg = note.text.match(/<([\w]+)[^>]*>(.*?)<\/\1>/g) const textData = ProcessText.deduceNoteTitle(note.text)
//Pull out first html tag contents, that is the title // console.log(textData)
if(reg != null && reg[0]){
note.title = reg[0] //First line from HTML
} else {
note.title = note.text //Entire note
}
//Clean up html title
note.title = ProcessText.removeHtml(note.title)
//Generate Subtext
note.subtext = ''
if(note.text != '' && note.title != ''){
note.subtext = ProcessText.removeHtml(note.text)
.substring(note.title.length)
}
note.title = textData.title
note.subtext = textData.sub
note.titleLength = textData.titleLength
note.subtextLength = textData.subtextLength
note.note_highlights = [] note.note_highlights = []
note.attachment_highlights = [] note.attachment_highlights = []
note.tag_highlights = [] note.tag_highlights = []
//Push in solr highlights //Push in search highlights
if(highlights && highlights[note.id]){ if(highlights && highlights[note.id]){
note['note_highlights'] = [highlights[note.id]] note['note_highlights'] = [highlights[note.id]]
} }
//Clear out note.text before sending it to front end //Clear out note.text before sending it to front end, its being used in title and subtext
delete note.text delete note.text
}) })
@ -396,6 +475,13 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
return resolve(returnData) return resolve(returnData)
} }
//Return all notes, tags are not being searched
// if tags are being searched, continue
// if notes are being filtered, return tags
if(searchTags.length == 0 && returnTagResults == false){
return resolve(returnData)
}
//Only show tags of selected notes //Only show tags of selected notes
db.promise() db.promise()
.query(`SELECT tag.id, tag.text, count(tag.id) as usages FROM note_tag .query(`SELECT tag.id, tag.text, count(tag.id) as usages FROM note_tag

View File

@ -2,6 +2,21 @@ let db = require('@config/database')
let Tag = module.exports = {} let Tag = module.exports = {}
Tag.userTags = (userId) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`
SELECT tag.id, text, COUNT(note_tag.note_id) as usages FROM tag
JOIN note_tag ON tag.id = note_tag.tag_id
WHERE note_tag.user_id = ?
GROUP BY tag.id
ORDER BY usages DESC
`, [userId])
.then( (rows, fields) => {
resolve(rows[0])
})
})
}
Tag.removeTagFromNote = (userId, tagId) => { Tag.removeTagFromNote = (userId, tagId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -1,7 +1,7 @@
let express = require('express') let express = require('express')
var multer = require('multer') var multer = require('multer')
var upload = multer({ dest: '../staticFiles/' }) var upload = multer({ dest: '../staticFiles/' }) //@TODO make this a global value
let router = express.Router() let router = express.Router()
let Attachment = require('@models/Attachment'); let Attachment = require('@models/Attachment');

View File

@ -38,6 +38,15 @@ router.post('/search', function (req, res) {
.then( notesAndTags => res.send(notesAndTags)) .then( notesAndTags => res.send(notesAndTags))
}) })
router.post('/difftext', function (req, res) {
Notes.getDiffText(userId, req.body.noteId, req.body.text, req.body.updated)
.then( fullDiffText => {
//Response should be full diff text
res.send(fullDiffText)
})
})
//Reindex all notes. Not a very good function, not public //Reindex all notes. Not a very good function, not public
router.get('/reindex5yu43prchuj903mrc', function (req, res) { router.get('/reindex5yu43prchuj903mrc', function (req, res) {

View File

@ -42,4 +42,10 @@ router.post('/get', function (req, res) {
.then( data => res.send(data) ) .then( data => res.send(data) )
}) })
//Get all the tags for this user in order of usage
router.post('/usertags', function (req, res) {
Tags.userTags(userId)
.then( data => res.send(data) )
})
module.exports = router module.exports = router