Better formatting and tag stripping for note title display

cleaned up interface a bunch
allow for opening of two notes at once
Escape closes note
Added global helper class and time ago function
Time ago function displays on main page and in note
Removed tab button creating tabbed spaces in document
Simplified save text
This commit is contained in:
Max G 2019-07-20 23:07:22 +00:00
parent 61754fe290
commit e6c16f3d37
7 changed files with 204 additions and 61 deletions

View File

@ -14,10 +14,8 @@
<script> <script>
//Attach event bus to main vue object, all components will inherit event bus
import EventBus from './EventBus'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
export default { export default {
@ -35,7 +33,6 @@ export default {
} }
}, },
mounted: function(){ mounted: function(){
// console.log(this.$bus)
}, },
computed: { computed: {
loggedIn () { loggedIn () {

62
client/src/Helpers.js Normal file
View File

@ -0,0 +1,62 @@
import Vue from 'vue'
const helpers = {}
helpers.timeAgo = (time) => {
const time_formats = [
[ 60, 'seconds', 1 ],
[ 120, '1 minute ago', '1 minute from now' ],
[ 3600, 'minutes', 60 ],
[ 7200, '1 hour ago', '1 hour from now' ],
[ 86400, 'hours', 3600 ],
[ 172800, 'yesterday', 'tomorrow' ],
[ 604800, 'days', 86400 ],
[ 1209600, 'last week', 'next week' ],
[ 2419200, 'weeks', 604800 ],
[ 4838400, 'last month', 'next month' ],
[ 29030400, 'months', 2419200 ],
[ 58060800, 'last year', 'next year' ],
[ 2903040000, 'years', 29030400 ],
[ 5806080000, 'last century', 'next century' ],
[ 58060800000, 'centuries', 2903040000 ]
]
//How many seconds ago was input event time?
let seconds = Math.round((+new Date)/1000) - time
let token = 'ago'
let list_choice = 1
if (seconds == 0) {
return 'Just now'
}
if (seconds < 0) {
seconds = Math.abs(seconds)
token = 'from now'
list_choice = 2
}
let i = 0
let format = null
while (format = time_formats[i++]){
if (seconds < format[0]) {
if (typeof format[2] == 'string') {
return format[list_choice]
} else {
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token
}
}
}
return time
}
const plugin = {
install () {
Vue.helpers = helpers
Vue.prototype.$helpers = helpers
}
}
Vue.use(plugin)

View File

@ -1,5 +1,5 @@
<template> <template>
<div id="InputNotes" class="master-note-edit"> <div id="InputNotes" class="master-note-edit" :class="[ 'position-'+position ]" @keyup.esc="close">
<ckeditor ref="main-edit" <ckeditor ref="main-edit"
:editor="editor" @ready="onReady" v-model="noteText" :config="editorConfig" v-on:blur="save"></ckeditor> :editor="editor" @ready="onReady" v-model="noteText" :config="editorConfig" v-on:blur="save"></ckeditor>
@ -7,10 +7,11 @@
<div class="ui buttons"> <div class="ui buttons">
<div class="ui green button">{{statusText}}</div> <div class="ui green button">{{statusText}}</div>
<div class="ui button">Delete</div> <div class="ui button">Delete</div>
<div v-on:click="close" class="ui button">Close</div> <div v-on:click="close" class="ui button">Close (ESC)</div>
<div class="ui disabled button">{{lastNoteHash}}</div>
<div class="ui disabled button">Last Update: {{updated}}</div>
</div> </div>
<p>
Last Updated: {{$helpers.timeAgo(updated)}}
</p>
<div class="ui segment"> <div class="ui segment">
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/> <note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/>
@ -47,7 +48,7 @@
export default { export default {
name: 'InputNotes', name: 'InputNotes',
props: [ 'noteid' ], props: [ 'noteid', 'position' ],
components:{ components:{
'note-tag-edit': require('./NoteTagEdit.vue').default 'note-tag-edit': require('./NoteTagEdit.vue').default
}, },
@ -57,6 +58,7 @@
noteText: '', noteText: '',
statusText: 'Save', statusText: 'Save',
lastNoteHash: null, lastNoteHash: null,
lastSaved: 0,
updated: 'Never', updated: 'Never',
editDebounce: null, editDebounce: null,
keyPressesCounter: 0, keyPressesCounter: 0,
@ -120,7 +122,6 @@
onReady(editor){ onReady(editor){
let vm = this let vm = this
console.log(vm)
// Insert the toolbar before the editable area. // Insert the toolbar before the editable area.
editor.ui.getEditableElement().parentElement.insertBefore( editor.ui.getEditableElement().parentElement.insertBefore(
@ -145,13 +146,15 @@
//Save after 20 keystrokes //Save after 20 keystrokes
vm.keyPressesCounter = (vm.keyPressesCounter + 1) vm.keyPressesCounter = (vm.keyPressesCounter + 1)
if(vm.keyPressesCounter > 30){ if(vm.keyPressesCounter > 30){
vm.keyPressesCounter = 0
vm.save() vm.save()
} }
//Optional data bindings for tab key
if( (data.keyCode == 9) && viewDocument.isFocused ){ if( (data.keyCode == 9) && viewDocument.isFocused ){
//Insert 5 spaces to simulate tab //Insert 5 spaces to simulate tab
editor.execute( 'input', { text: " " } ); //editor.execute( 'input', { text: " " } );
evt.stop(); // Prevent executing the default handler. evt.stop(); // Prevent executing the default handler.
data.preventDefault(); data.preventDefault();
@ -162,9 +165,6 @@
}, },
save(){ save(){
console.log('Save, ', this.keyPressesCounter)
this.keyPressesCounter = 0
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
@ -172,21 +172,22 @@
return return
} }
const postData = { const postData = {
'noteId':this.currentNoteId, 'noteId':this.currentNoteId,
'text': this.noteText 'text': this.noteText
} }
let vm = this let vm = this
//Only notify user if saving - may help with debugging in the future
vm.statusText = 'Saving'
axios.post('/api/notes/update', postData).then( response => { axios.post('/api/notes/update', postData).then( response => {
vm.statusText = 'Saved' vm.statusText = 'Save'
vm.updated = Math.round((+new Date)/1000)
//Update last saved note hash //Update last saved note hash
vm.lastNoteHash = vm.hashString(vm.noteText) vm.lastNoteHash = vm.hashString(vm.noteText)
setTimeout(() => {
vm.statusText = 'Save'
}, 5000)
}) })
}, },
hashString(text){ hashString(text){
@ -202,7 +203,7 @@
return hash; return hash;
}, },
close(){ close(){
this.$bus.$emit('close_active_note') this.$bus.$emit('close_active_note', this.position)
} }
} }
} }
@ -212,14 +213,24 @@
.master-note-edit { .master-note-edit {
position: fixed; position: fixed;
left: 10%;
right: 10%;
bottom: 0; bottom: 0;
background: green; background: white;
height: 98vh; height: 99vh;
border-top-right-radius: 4px;
border-top-left-radius: 4px;
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1); box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
} }
/* One note open, in the middle of the screen */
.master-note-edit.position-0 {
left: 30%;
right: 1%;
}
/* Two Notes Open, each takes up 50% of the space */
.master-note-edit.position-1 {
left: 50%;
right: 0%;
}
.master-note-edit.position-2 {
left: 0%;
right: 50%;
}
</style> </style>

View File

@ -4,7 +4,7 @@
<search-bar /> <search-bar />
</div> </div>
<input-notes v-if="activeNoteId != null" :noteid="activeNoteId"></input-notes>
</div> </div>
</template> </template>
@ -17,7 +17,6 @@
export default { export default {
name: 'Notes', name: 'Notes',
components:{ components:{
'input-notes': require('./InputNotes.vue').default,
'search-bar': require('./SearchBar.vue').default 'search-bar': require('./SearchBar.vue').default
}, },
data () { data () {
@ -27,21 +26,13 @@
} }
}, },
beforeMount(){ beforeMount(){
this.$bus.$on('close_active_note', () => {
this.activeNoteId = null
})
this.$bus.$on('open_note', openNoteId => {
this.activeNoteId = openNoteId
})
}, },
mounted: function() { mounted: function() {
//this.getLatest() //this.getLatest()
}, },
methods: { methods: {
openNote(id){
this.activeNoteId = id
}
} }
} }
</script> </script>

View File

@ -5,29 +5,45 @@
<div class="ui grid"> <div class="ui grid">
<!-- search menu --> <!-- search menu -->
<div class="ui row"> <div class="ui row">
<div @click="createNote" class="ui green button"> <div class="ui two wide column">
<div @click="createNote" class="ui fluid green button">
<i class="plus icon"></i> <i class="plus icon"></i>
New Note New Note
</div> </div>
</div>
<div class="ui five wide column">
<div class="ui form">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" /> <input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" />
<div class="ui button" @click="reset">Reset</div> </div>
</div>
</div> </div>
<div class="ui row"> <div class="ui row">
<div class="ui two wide column"> <div class="ui two wide column">
<div class="ui basic fluid button" @click="reset">Reset</div>
<div class="ui divider"></div>
<div class="ui clickable basic fluid large label" v-for="tag in commonTags" @click="toggleTagFilter(tag.id)" <div class="ui clickable basic fluid large label" v-for="tag in commonTags" @click="toggleTagFilter(tag.id)"
:class="{ 'green':(searchTags.includes(tag.id)) }"> :class="{ 'green':(searchTags.includes(tag.id)) }">
{{ucWords(tag.text)}} <div class="detail">{{tag.usages}}</div> {{ucWords(tag.text)}} <div class="detail">{{tag.usages}}</div>
</div> </div>
</div> </div>
<div class="ui fourteen wide column"> <div class="ui fourteen wide column">
<h2>Notes ({{notes.length}})</h2>
<div v-if="notes !== null"> <div v-if="notes !== null">
<div class="ui vertical segment clickable" v-for="note in notes" v-on:click="openNote(note.id)" v-html="note.text"> <div class="ui vertical segment clickable" v-for="note in notes" v-on:click="openNote(note.id)">
<h3>{{note.text}}</h3>
<p>Edited: {{$helpers.timeAgo(note.updated)}}</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<input-notes v-if="activeNoteId1 != null" :noteid="activeNoteId1" :position="activeNote1Position" />
<input-notes v-if="activeNoteId2 != null" :noteid="activeNoteId2" :position="activeNote2Position" />
</div> </div>
</template> </template>
@ -37,23 +53,75 @@
export default { export default {
name: 'SearchBar', name: 'SearchBar',
components: {
'input-notes': require('./InputNotes.vue').default,
},
data () { data () {
return { return {
initComponent: true, initComponent: true,
commonTags: [], commonTags: [],
searchTerm: '', searchTerm: '',
searchTags: [], searchTags: [],
notes: null, notes: [],
searchDebounce: null searchDebounce: null,
//Currently open notes in app
activeNoteId1: null,
activeNoteId2: null,
//Position determines how note is Positioned
activeNote1Position: 0,
activeNote2Position: 0
} }
}, },
beforeMount(){ beforeMount(){
this.$bus.$on('close_active_note', position => {
this.closeNote(position)
})
}, },
mounted() { mounted() {
this.search() this.search()
}, },
methods: { methods: {
openNote(id){
//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
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
}
},
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
}
this.activeNote1Position = 0
this.activeNote2Position = 0
this.search()
},
toggleTagFilter(tagId){ toggleTagFilter(tagId){
if(this.searchTags.includes(tagId)){ if(this.searchTags.includes(tagId)){
@ -99,10 +167,6 @@
} }
}) })
}, },
openNote(noteId){
//Emit open note event
this.$bus.$emit('open_note', noteId)
},
ucWords(str){ ucWords(str){
return (str + '') return (str + '')
.replace(/^(.)|\s+(.)/g, function ($1) { .replace(/^(.)|\s+(.)/g, function ($1) {

View File

@ -9,6 +9,10 @@ import store from './stores/mainStore';
import App from './App' import App from './App'
import router from './router' import router from './router'
//Attach event bus to main vue object, all components will inherit event bus
import EventBus from './EventBus'
import Helpers from './Helpers'
import CKEditor from '@ckeditor/ckeditor5-vue'; import CKEditor from '@ckeditor/ckeditor5-vue';
Vue.use( CKEditor ) Vue.use( CKEditor )

View File

@ -5,7 +5,7 @@ let Notes = module.exports = {}
Notes.create = (userId, noteText) => { Notes.create = (userId, noteText) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const created = new Date().toISOString().slice(0, 19).replace('T', ' ') const created = Math.round((+new Date)/1000)
db.promise() db.promise()
.query('INSERT INTO notes (user, text, created) VALUES (?,?,?)', [userId, noteText, created]) .query('INSERT INTO notes (user, text, created) VALUES (?,?,?)', [userId, noteText, created])
@ -18,11 +18,12 @@ Notes.create = (userId, noteText) => {
Notes.update = (userId, noteId, noteText) => { Notes.update = (userId, noteId, noteText) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const now = new Date().toISOString().slice(0, 19).replace('T', ' ')
const now = Math.round((+new Date)/1000)
db.promise() db.promise()
.query('UPDATE notes SET text = ?, updated = ? WHERE id = ? AND user = ? LIMIT 1', [noteText, now, noteId, userId]) .query('UPDATE notes SET text = ?, updated = ? WHERE id = ? AND user = ? LIMIT 1', [noteText, now, noteId, userId])
.then((rows, fields) => { .then((rows, fields) => {
console.log(rows)
resolve(rows[0]) resolve(rows[0])
}) })
.catch(console.log) .catch(console.log)
@ -63,7 +64,7 @@ Notes.search = (userId, searchQuery, searchTags) => {
//Default note lookup gets all notes //Default note lookup gets all notes
let noteSearchQuery = ` let noteSearchQuery = `
SELECT notes.id, SUBSTRING(text, 1, 100) as text SELECT notes.id, SUBSTRING(text, 1, 200) as text, updated
FROM notes FROM notes
LEFT JOIN notes_tags ON (notes.id = notes_tags.note_id) LEFT JOIN notes_tags ON (notes.id = notes_tags.note_id)
WHERE user = ?` WHERE user = ?`
@ -81,7 +82,7 @@ Notes.search = (userId, searchQuery, searchTags) => {
} }
//Finish up note query //Finish up note query
noteSearchQuery += ' GROUP BY notes.id ORDER BY updated DESC, created DESC' noteSearchQuery += ' GROUP BY notes.id ORDER BY updated DESC, created DESC, id DESC'
//Define return data objects //Define return data objects
let returnData = { let returnData = {
@ -96,10 +97,23 @@ Notes.search = (userId, searchQuery, searchTags) => {
//Push all notes //Push all notes
returnData['notes'] = noteRows[0] returnData['notes'] = noteRows[0]
//Pull Tags off of selected notes //pull out all note ids so we can fetch all tags for those notes
let noteIds = [] let noteIds = []
returnData['notes'].forEach(note => { returnData['notes'].forEach(note => {
//Grab note ID for finding tags
noteIds.push(note.id) noteIds.push(note.id)
//Attempt to pull string out of first tag in note
let reg = note.text.match(/<([\w]+)[^>]*>(.*?)<\/\1>/)
if(reg != null){
note.text = reg[2]
}
//Return all notes with HTML tags pulled out
note.text = note.text
.replace(/&[#A-Za-z0-9]+;/g,'') //Rip out all HTML entities
.replace(/<[^>]+>/g, '') //Rip out all HTML tags
}) })
//If no notes are returned, there are no tags, return empty //If no notes are returned, there are no tags, return empty