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') },
],
},
hot: true,
// hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
compress: true,
host: HOST || config.dev.host,

View File

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

View File

@ -1,21 +1,49 @@
<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 green button">{{statusText}}</div>
<div class="ui right floated green button">{{statusText}}</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>
<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)}}
</p>
</p> -->
<div class="ui segment">
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/>
</div>
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/>
<div class="ui segment" v-if="false">
Block formatting
@ -62,6 +90,8 @@
updated: 'Never',
editDebounce: null,
keyPressesCounter: 0,
fancyInput: 0, //Default to basic text edit. Upgrade if set to 1
color: '#FFF',
editor: DecoupledEditor,
editorConfig: {
@ -97,18 +127,45 @@
this.loadNote(this.noteid)
},
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){
let vm = this
//Component is activated with NoteId in place, lookup text with associated ID
if(this.$store.getters.getLoggedIn){
axios.post('/api/notes/get', {'noteId': noteId})
.then(response => {
//Set up local data
vm.currentNoteId = noteId
vm.noteText = response.data.text
vm.updated = response.data.updated
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
vm.$nextTick(() => {
// vm.$refs['custom-input'].focus()
@ -138,17 +195,7 @@
//Insert 5 spaces when tab is pressed
viewDocument.on( 'keyup', ( evt, data ) => {
//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()
}
vm.onKeyup(event)
//Optional data bindings for tab key
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(){
clearTimeout(this.editDebounce)
//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
}
const postData = {
'noteId':this.currentNoteId,
'text': this.noteText
'text': this.noteText,
'fancyInput': this.fancyInput,
'color': this.color
}
let vm = this
@ -188,7 +250,7 @@
//Update last saved note hash
vm.lastNoteHash = vm.hashString(vm.noteText)
})
})
},
hashString(text){
var hash = 0;
@ -203,6 +265,7 @@
return hash;
},
close(){
this.save()
this.$bus.$emit('close_active_note', this.position)
}
}
@ -211,11 +274,30 @@
<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 {
position: fixed;
bottom: 0;
background: white;
height: 99vh;
height: 100vh;
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
}
/* One note open, in the middle of the screen */
@ -223,6 +305,15 @@
left: 30%;
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 */
.master-note-edit.position-1 {
left: 50%;

View File

@ -1,10 +1,20 @@
<template>
<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>
</div>
<input v-model="newTagInput" v-on:keyup.enter="addTag" placeholder="Add Tag" />
<div class="ui divider"></div>
<div class="ui form">
<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>
</template>
@ -18,7 +28,11 @@
data () {
return {
tags: null,
newTagInput: ''
newTagInput: '',
typeDebounce: null,
suggestions: [],
selection: 0
}
},
beforeMount(){
@ -37,9 +51,80 @@
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 = {
'tagText':this.newTagInput,
'noteId':this.noteId
@ -48,9 +133,21 @@
axios.post('/api/tags/addtonote', postData)
.then(response => {
vm.newTagInput = ''
vm.clearSuggestions()
vm.getTags()
})
},
onKeyup(){
if(this.newTagInput == ''){
this.clearSuggestions()
}
},
onBlur(){
let vm = this
setTimeout(i => {
vm.clearSuggestions()
}, 200)
},
removeTag(tagId){
let postData = {
@ -63,6 +160,10 @@
vm.getTags()
})
},
clearSuggestions(){
this.suggestions = []
this.selection = -1 //No selections
},
ucWords(str){
return (str + '')
.replace(/^(.)|\s+(.)/g, function ($1) {
@ -71,4 +172,29 @@
}
}
}
</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 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 -->
<div class="ui row">
<div class="ui large screen only row">
<div class="ui two wide column">
<div @click="createNote" class="ui fluid green button">
<i class="plus icon"></i>
New Note
</div>
</div>
<div class="ui five wide column">
<div class="ui form">
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" />
</div>
</div>
</div>
<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 divider"></div>
<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>
</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>
<div v-if="notes !== null">
<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>
<note-title-display-card
v-for="note in notes"
:onClick="openNote"
:data="note"
:key="note.id + note.color"
/>
</div>
</div>
</div>
@ -55,6 +79,7 @@
name: 'SearchBar',
components: {
'input-notes': require('./InputNotes.vue').default,
'note-title-display-card': require('./NoteTitleDisplayCard.vue').default,
},
data () {
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) => {
const now = Math.round((+new Date)/1000)
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) => {
resolve(rows[0])
})
@ -39,7 +39,7 @@ Notes.delete = (userId, noteId) => {
Notes.get = (userId, noteId) => {
return new Promise((resolve, reject) => {
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) => {
resolve(rows[0][0])
})
@ -64,7 +64,7 @@ Notes.search = (userId, searchQuery, searchTags) => {
//Default note lookup gets all notes
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
LEFT JOIN notes_tags ON (notes.id = notes_tags.note_id)
WHERE user = ?`

View File

@ -105,4 +105,27 @@ Tags.lookup = (tagText) => {
})
.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) {
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}) )
})

View File

@ -13,6 +13,12 @@ router.use(function setUserId (req, res, 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
router.post('/addtonote', function (req, res) {
Tags.addToNote(userId, req.body.noteId, req.body.tagText.toLowerCase())