Cleaned up some display issues that probably still need work

Added some colors to the notes and basic support for displaying the colors on the main list

Added a toggle to disable the fancy text editor and just use a basic textarea

Added some mobile styles with much better support for smaller screens

Added tag suggestions based on user input, excluding tags from current note, only using tags user has put into system

Cleaned and refactored a bunch of stuff
This commit is contained in:
Max G 2019-07-21 16:28:07 +00:00
parent e6c16f3d37
commit e52ae65a42
10 changed files with 371 additions and 50 deletions

View File

@ -28,7 +28,7 @@ const devWebpackConfig = merge(baseWebpackConfig, {
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
], ],
}, },
hot: true, // hot: true,
contentBase: false, // since we use CopyWebpackPlugin. contentBase: false, // since we use CopyWebpackPlugin.
compress: true, compress: true,
host: HOST || config.dev.host, host: HOST || config.dev.host,

View File

@ -1,3 +1,7 @@
/*body, h3, h2, h1, p {
color: #3d3d3d;
}*/
.clickable { .clickable {
cursor: pointer; cursor: pointer;
} }
@ -6,5 +10,9 @@
font-family: 'Open Sans' !important; font-family: 'Open Sans' !important;
font-size: 1.3rem !important; font-size: 1.3rem !important;
background: white; background: white;
height: calc(100% - 177px); height: 100%;
overflow: hidden;
}
.ui.white.button {
background: #FFF;
} }

View File

@ -1,21 +1,49 @@
<template> <template>
<div id="InputNotes" class="master-note-edit" :class="[ 'position-'+position ]" @keyup.esc="close"> <div id="InputNotes" class="master-note-edit" :class="[ 'position-'+position ]" @keyup.esc="close" :style="{'background-color':color}">
<ckeditor ref="main-edit"
:editor="editor" @ready="onReady" v-model="noteText" :config="editorConfig" v-on:blur="save"></ckeditor> <div v-if="fancyInput == 1" class="textarea-height no-flow">
<ckeditor ref="main-edit"
:editor="editor" @ready="onReady" v-model="noteText" :config="editorConfig" v-on:blur="save" />
</div>
<textarea
class="textarea-height raw-edit"
v-if="fancyInput == 0"
v-model="noteText"
v-on:blur="save"
v-on:keyup="onKeyup"
/>
<div class="ui buttons"> <div class="ui buttons">
<div class="ui green button">{{statusText}}</div> <div class="ui right floated 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 (ESC)</div> <div @click="close" class="ui button">Close (ESC)</div>
<div @click="onToggleFancyInput" class="ui button">
Fancy ({{fancyInput?'On':'Off'}})
</div>
</div> </div>
<p> <div class="ui buttons">
<button @click="onChangeColor" class="ui icon white button"></button>
<button @click="onChangeColor" class="ui icon red button"></button>
<button @click="onChangeColor" class="ui icon orange button"></button>
<button @click="onChangeColor" class="ui icon yellow button"></button>
<button @click="onChangeColor" class="ui icon olive button"></button>
<button @click="onChangeColor" class="ui icon green button"></button>
<button @click="onChangeColor" class="ui icon teal button"></button>
<button @click="onChangeColor" class="ui icon blue button"></button>
<button @click="onChangeColor" class="ui icon violet button"></button>
<button @click="onChangeColor" class="ui icon purple button"></button>
<button @click="onChangeColor" class="ui icon pink button"></button>
<button @click="onChangeColor" class="ui icon brown button"></button>
<button @click="onChangeColor" class="ui icon grey button"></button>
<button @click="onChangeColor" class="ui icon black button"></button>
</div>
<!-- <p>
Last Updated: {{$helpers.timeAgo(updated)}} Last Updated: {{$helpers.timeAgo(updated)}}
</p> </p> -->
<div class="ui segment"> <note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/>
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/>
</div>
<div class="ui segment" v-if="false"> <div class="ui segment" v-if="false">
Block formatting Block formatting
@ -62,6 +90,8 @@
updated: 'Never', updated: 'Never',
editDebounce: null, editDebounce: null,
keyPressesCounter: 0, keyPressesCounter: 0,
fancyInput: 0, //Default to basic text edit. Upgrade if set to 1
color: '#FFF',
editor: DecoupledEditor, editor: DecoupledEditor,
editorConfig: { editorConfig: {
@ -97,18 +127,45 @@
this.loadNote(this.noteid) this.loadNote(this.noteid)
}, },
methods: { methods: {
onToggleFancyInput(){
if(this.fancyInput == 0){
this.fancyInput = 1
} else {
this.fancyInput = 0;
}
//Update last note hash, this will tell note to save next update
this.lastNoteHash = 0
},
onChangeColor(event){
//Grab the color of the button clicked
const style = getComputedStyle(event.target)
this.color = style['background-color']
if(this.color == "rgb(255, 255, 255)" || this.color == '#FFF'){
this.color = null
}
this.lastNoteHash = 0 //Update hash to force note update on next save
this.save()
},
loadNote(noteId){ loadNote(noteId){
let vm = this let vm = this
//Component is activated with NoteId in place, lookup text with associated ID //Component is activated with NoteId in place, lookup text with associated ID
if(this.$store.getters.getLoggedIn){ if(this.$store.getters.getLoggedIn){
axios.post('/api/notes/get', {'noteId': noteId}) axios.post('/api/notes/get', {'noteId': noteId})
.then(response => { .then(response => {
//Set up local data //Set up local data
vm.currentNoteId = noteId vm.currentNoteId = noteId
vm.noteText = response.data.text vm.noteText = response.data.text
vm.updated = response.data.updated vm.updated = response.data.updated
vm.lastNoteHash = vm.hashString(response.data.text) vm.lastNoteHash = vm.hashString(response.data.text)
if(response.data.raw_input == 1){
this.fancyInput = 1
}
//Put focus on note, at the end of the note text //Put focus on note, at the end of the note text
vm.$nextTick(() => { vm.$nextTick(() => {
// vm.$refs['custom-input'].focus() // vm.$refs['custom-input'].focus()
@ -138,17 +195,7 @@
//Insert 5 spaces when tab is pressed //Insert 5 spaces when tab is pressed
viewDocument.on( 'keyup', ( evt, data ) => { viewDocument.on( 'keyup', ( evt, data ) => {
//Each note, save after 5 seconds, focus lost or 30 characters typed. vm.onKeyup(event)
clearTimeout(vm.editDebounce)
vm.editDebounce = setTimeout(() => {
vm.save()
}, 5000)
//Save after 20 keystrokes
vm.keyPressesCounter = (vm.keyPressesCounter + 1)
if(vm.keyPressesCounter > 30){
vm.keyPressesCounter = 0
vm.save()
}
//Optional data bindings for tab key //Optional data bindings for tab key
if( (data.keyCode == 9) && viewDocument.isFocused ){ if( (data.keyCode == 9) && viewDocument.isFocused ){
@ -163,19 +210,34 @@
} ); } );
}, },
onKeyup(){
let vm = this
//Each note, save after 5 seconds, focus lost or 30 characters typed.
clearTimeout(vm.editDebounce)
vm.editDebounce = setTimeout(() => {
vm.save()
}, 5000)
//Save after 20 keystrokes
vm.keyPressesCounter = (vm.keyPressesCounter + 1)
if(vm.keyPressesCounter > 30){
vm.keyPressesCounter = 0
vm.save()
}
},
save(){ 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
if(this.lastNoteHash == this.hashString(this.noteText)){ if( this.lastNoteHash == this.hashString(this.noteText) ){
return return
} }
const postData = { const postData = {
'noteId':this.currentNoteId, 'noteId':this.currentNoteId,
'text': this.noteText 'text': this.noteText,
'fancyInput': this.fancyInput,
'color': this.color
} }
let vm = this let vm = this
@ -203,6 +265,7 @@
return hash; return hash;
}, },
close(){ close(){
this.save()
this.$bus.$emit('close_active_note', this.position) this.$bus.$emit('close_active_note', this.position)
} }
} }
@ -211,11 +274,30 @@
<style type="text/css" scoped> <style type="text/css" scoped>
.textarea-height {
height: calc(100% - 177px);
}
.no-flow {
overflow: hidden;
}
.raw-edit {
font-family: 'Open Sans' !important;
font-size: 1.3rem !important;
background: white;
width: 100%;
resize: none;
padding: 15px;
border: 1px solid;
}
/* container styles change based on mobile and number of open screens */
.master-note-edit { .master-note-edit {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
background: white; background: white;
height: 99vh; height: 100vh;
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 */ /* One note open, in the middle of the screen */
@ -223,6 +305,15 @@
left: 30%; left: 30%;
right: 1%; right: 1%;
} }
@media only screen and (max-width: 740px) {
.master-note-edit.position-0 {
left: 0;
right: 0;
top: 0;
bottom: 0;
}
}
/* Two Notes Open, each takes up 50% of the space */ /* Two Notes Open, each takes up 50% of the space */
.master-note-edit.position-1 { .master-note-edit.position-1 {
left: 50%; left: 50%;

View File

@ -1,10 +1,20 @@
<template> <template>
<div> <div>
<div class="ui icon large label" v-for="tag in tags"> <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> {{ucWords(tag.text)}} <i class="delete icon" v-on:click="removeTag(tag.id)"></i>
</div> </div>
<input v-model="newTagInput" v-on:keyup.enter="addTag" placeholder="Add Tag" /> <div class="ui form">
<div class="ui divider"></div> <input v-model="newTagInput" placeholder="Add Tag"
v-on:keydown="tagInput"
v-on:keyup="onKeyup"
v-on:blur="onBlur"
/>
<div class="suggestion-box" v-if="suggestions.length > 0">
<div class="suggestion-item" v-for="(item, index) in suggestions" :class="{ 'active':(index == selection) }" v-on:click="onClickTag(index)">
{{ucWords(item.text)}} <span class="suggestion-tip" v-if="index == selection">Press Enter to add</span>
</div>
</div>
</div>
</div> </div>
</template> </template>
@ -18,7 +28,11 @@
data () { data () {
return { return {
tags: null, tags: null,
newTagInput: '' newTagInput: '',
typeDebounce: null,
suggestions: [],
selection: 0
} }
}, },
beforeMount(){ beforeMount(){
@ -37,9 +51,80 @@
vm.tags = response.data vm.tags = response.data
}) })
}, },
addTag(event){ tagInput(event){
let vm = this
if(this.newTagInput.length == 0){
this.clearSuggestions()
}
//Cancel any of the following key events
const code = event.keyCode
if([38, 40, 13, 9].includes(code)){
event.preventDefault()
}
//Up - 38 - Go Up suggestion
if(code == 40){
this.selection ++
if(this.selection >= this.suggestions.length){
this.selection = -1 //No selection made
}
console.log('Current Selection Index: ', this.selection)
return
}
//Down - 40 - Go Down Suggestion
if(code == 38){
this.selection --
if(this.selection < -1){
this.selection = this.suggestions.length -1 //No selection made
}
console.log('Current Selection Index: ', this.selection)
return;
}
//Enter - 13 - Execute addTag() function
if(code == 13){
//If an item from list is selected, make that the text
if(this.selection > -1){
this.newTagInput = this.suggestions[vm.selection].text
}
this.addTag()
return
}
//Tab - 9 - Fill space with suggestion
//Anything else, perform search
clearTimeout(this.typeDebounce)
this.typeDebounce = setTimeout( () => {
if(event.target.value.length > 0){
const postData = {
'tagText':vm.newTagInput,
'noteId':vm.noteId
}
axios.post('/api/tags/suggest', postData)
.then(response => {
vm.suggestions = response.data
vm.selection = -1 //Nothing selected
})
}
}, 300)
},
onClickTag(index){
this.newTagInput = this.suggestions[index].text
this.addTag()
},
addTag(){
//Don't add blank or empty tags
if(this.newTagInput == ''){
return
}
//post to -> /api/addtagtonote
let postData = { let postData = {
'tagText':this.newTagInput, 'tagText':this.newTagInput,
'noteId':this.noteId 'noteId':this.noteId
@ -48,9 +133,21 @@
axios.post('/api/tags/addtonote', postData) axios.post('/api/tags/addtonote', postData)
.then(response => { .then(response => {
vm.newTagInput = '' vm.newTagInput = ''
vm.clearSuggestions()
vm.getTags() vm.getTags()
}) })
}, },
onKeyup(){
if(this.newTagInput == ''){
this.clearSuggestions()
}
},
onBlur(){
let vm = this
setTimeout(i => {
vm.clearSuggestions()
}, 200)
},
removeTag(tagId){ removeTag(tagId){
let postData = { let postData = {
@ -63,6 +160,10 @@
vm.getTags() vm.getTags()
}) })
}, },
clearSuggestions(){
this.suggestions = []
this.selection = -1 //No selections
},
ucWords(str){ ucWords(str){
return (str + '') return (str + '')
.replace(/^(.)|\s+(.)/g, function ($1) { .replace(/^(.)|\s+(.)/g, function ($1) {
@ -72,3 +173,28 @@
} }
} }
</script> </script>
<style type="text/css" scoped>
.suggestion-box {
position: absolute;
bottom: 38px;
width: 300px;
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
}
.suggestion-item {
width: 100%;
height: 40px;
padding: 10px 15px;
cursor: pointer;
background: white;
}
.suggestion-item.active {
background: green;
color: white;
}
.suggestion-item + .suggestion-item {
border-top: 1px solid #DDD;
}
.suggestion-tip {
float: right;
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<div class="ui clickable segment" @click="onClick(note.id)" :style="{'background-color':color, 'color':fontColor}">
<h3>{{note.text}}</h3>
<p>Edited: {{$helpers.timeAgo(note.updated)}}</p>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'NoteTitleDisplayCard',
props: [ 'onClick', 'data' ],
data () {
return {
note: null,
color: '#FFF',
fontColor: '#000'
}
},
beforeMount(){
this.note = this.data
if(this.note.color != null && this.note.color != '#FFF'){
this.color = this.note.color
this.fontColor = '#FFF'
}
},
mounted() {
},
methods: {
yup(){
}
}
}
</script>
<style type="text/css" scoped>
.suggestion-box {
}
</style>

View File

@ -2,25 +2,45 @@
<div> <div>
<div class="ui grid"> <div class="ui equal width grid">
<!-- mobile search menu -->
<div class="ui mobile only row">
<!-- Small screen new note button -->
<div class="ui four wide column">
<div @click="createNote" class="ui fluid green icon button">
<i class="plus icon"></i>
</div>
</div>
<div class="ui twelve wide column">
<div class="ui form">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" />
</div>
</div>
</div>
<!-- search menu --> <!-- search menu -->
<div class="ui row"> <div class="ui large screen only row">
<div class="ui two wide column"> <div class="ui two wide column">
<div @click="createNote" class="ui fluid green button"> <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>
<div class="ui five wide column"> <div class="ui five wide column">
<div class="ui form"> <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> </div>
</div> </div>
</div> </div>
<div class="ui row"> <div class="ui row">
<div class="ui two wide column">
<!-- tags display -->
<div class="ui two wide large screen only column">
<div class="ui basic fluid button" @click="reset">Reset</div> <div class="ui basic fluid button" @click="reset">Reset</div>
<div class="ui divider"></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)"
@ -28,13 +48,17 @@
{{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">
<!-- Note title cards -->
<div class="ui fourteen wide computer sixteen wide mobile column">
<h2>Notes ({{notes.length}})</h2> <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)"> <note-title-display-card
<h3>{{note.text}}</h3> v-for="note in notes"
<p>Edited: {{$helpers.timeAgo(note.updated)}}</p> :onClick="openNote"
</div> :data="note"
:key="note.id + note.color"
/>
</div> </div>
</div> </div>
</div> </div>
@ -55,6 +79,7 @@
name: 'SearchBar', name: 'SearchBar',
components: { components: {
'input-notes': require('./InputNotes.vue').default, 'input-notes': require('./InputNotes.vue').default,
'note-title-display-card': require('./NoteTitleDisplayCard.vue').default,
}, },
data () { data () {
return { return {

View File

@ -16,13 +16,13 @@ Notes.create = (userId, noteText) => {
}) })
} }
Notes.update = (userId, noteId, noteText) => { Notes.update = (userId, noteId, noteText, fancyInput, color) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const now = Math.round((+new Date)/1000) 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 = ?, raw_input = ?, updated = ?, color = ? WHERE id = ? AND user = ? LIMIT 1', [noteText, fancyInput, now, color, noteId, userId])
.then((rows, fields) => { .then((rows, fields) => {
resolve(rows[0]) resolve(rows[0])
}) })
@ -39,7 +39,7 @@ Notes.delete = (userId, noteId) => {
Notes.get = (userId, noteId) => { Notes.get = (userId, noteId) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise() db.promise()
.query('SELECT text, updated FROM notes WHERE user = ? AND id = ? LIMIT 1', [userId,noteId]) .query('SELECT text, updated, raw_input FROM notes WHERE user = ? AND id = ? LIMIT 1', [userId,noteId])
.then((rows, fields) => { .then((rows, fields) => {
resolve(rows[0][0]) resolve(rows[0][0])
}) })
@ -64,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, 200) as text, updated SELECT notes.id, SUBSTRING(text, 1, 200) as text, updated, color
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 = ?`

View File

@ -106,3 +106,26 @@ Tags.lookup = (tagText) => {
.catch(console.log) .catch(console.log)
}) })
} }
//Suggest note tags - don't suggest tags already on note
Tags.suggest = (userId, noteId, tagText) => {
tagText += '%'
return new Promise((resolve, reject) => {
db.promise()
.query(`SELECT text FROM notes_tags
JOIN tags ON notes_tags.tag_id = tags.id
WHERE notes_tags.user_id = ?
AND tags.text LIKE ?
AND notes_tags.tag_id NOT IN (
SELECT notes_tags.tag_id FROM notes_tags WHERE notes_tags.note_id = ?
)
GROUP BY text
LIMIT 6;`, [userId, tagText, noteId])
.then((rows, fields) => {
resolve(rows[0]) //Return new ID
})
.catch(console.log)
})
}

View File

@ -30,7 +30,7 @@ router.post('/create', function (req, res) {
}) })
router.post('/update', function (req, res) { router.post('/update', function (req, res) {
Notes.update(userId, req.body.noteId, req.body.text) Notes.update(userId, req.body.noteId, req.body.text, req.body.fancyInput, req.body.color)
.then( id => res.send({id}) ) .then( id => res.send({id}) )
}) })

View File

@ -13,6 +13,12 @@ router.use(function setUserId (req, res, next) {
next() next()
}) })
//Get the latest notes the user has created
router.post('/suggest', function (req, res) {
Tags.suggest(userId, req.body.noteId, req.body.tagText)
.then( data => res.send(data) )
})
//Get the latest notes the user has created //Get the latest notes the user has created
router.post('/addtonote', function (req, res) { router.post('/addtonote', function (req, res) {
Tags.addToNote(userId, req.body.noteId, req.body.tagText.toLowerCase()) Tags.addToNote(userId, req.body.noteId, req.body.tagText.toLowerCase())