* Made splash page dark and updated description

* Cleaned up unused things
* Updated squire which had a comment typo update...thats it
* Background color picker has matching colors and styles to text color picker
* Added new black theme
* Moved search to main page, show it on mobile and added options to push things to notes from search with experimental tag searching
* Added active note menu buttons based on cursor location in text
* Added more instant updating if app is open in two locations for the same user Scratch Pad and home page update with new notes and new text in real time
This commit is contained in:
Max G 2020-05-15 23:12:09 +00:00
parent 67b218329b
commit e87e8513bc
20 changed files with 526 additions and 271 deletions

View File

@ -10,13 +10,16 @@
<meta name="theme-color" content="#000" /> <meta name="theme-color" content="#000" />
<link rel="manifest" href="/api/static/assets/manifest.json"> <link rel="manifest" href="/api/static/assets/manifest.json">
<title>Solid Scribe - A Note Taking Website</title> <title>Solid Scribe - An easy, encrypted Note App</title>
</head> </head>
<body> <body>
<div id="app"> <div id="app">
<!-- placeholder data for scrapers with no JS --> <!-- placeholder data for scrapers with no JS -->
<style> <style>
body {
background-color: #212221;
color: #aeaeae;
}
.centered { .centered {
position: fixed; position: fixed;
top: 50%; top: 50%;
@ -37,7 +40,8 @@
<div class="centered"> <div class="centered">
<img class="logo" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo - if you can read this your connection is really slow"> <img class="logo" src="/api/static/assets/logo.svg" alt="Solid Scribe Logo - if you can read this your connection is really slow">
<h1>Solid Scribe</h1> <h1>Solid Scribe</h1>
<h3>Loading...</h3> <h3>An easy, encrypted Note App</h3>
<h4>Loading...</h4>
</div> </div>
<div class="scrape-info"> <div class="scrape-info">

View File

@ -1,5 +1,5 @@
<template> <template>
<div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode) }"> <div id="app" :class="{ 'night-mode':($store.getters.getIsNightMode == 2) }">
<global-site-menu /> <global-site-menu />
@ -30,7 +30,6 @@ export default {
//Puts token into state on page load //Puts token into state on page load
let token = localStorage.getItem('loginToken') let token = localStorage.getItem('loginToken')
let username = localStorage.getItem('username') let username = localStorage.getItem('username')
let masterKey = localStorage.getItem('masterKey')
// const socket = io({ path:'/socket' }); // const socket = io({ path:'/socket' });
const socket = this.$io const socket = this.$io
@ -45,13 +44,14 @@ export default {
this.$store.commit('detectIsUserOnMobile') this.$store.commit('detectIsUserOnMobile')
//Set color theme based on local storage //Set color theme based on local storage
if(localStorage.getItem('nightMode') == 'true'){ const themeNumber = localStorage.getItem('nightMode')
this.$store.commit('toggleNightMode') if(themeNumber != null){
this.$store.commit('toggleNightMode', themeNumber)
} }
//Put user data into global store on load //Put user data into global store on load
if(token){ if(token){
this.$store.commit('setLoginToken', {token, username, masterKey}) this.$store.commit('setLoginToken', {token, username})
} }
}, },

View File

@ -392,7 +392,7 @@ function createElement ( doc, tag, props, children ) {
function fixCursor ( node, root ) { function fixCursor ( node, root ) {
// In Webkit and Gecko, block level elements are collapsed and // In Webkit and Gecko, block level elements are collapsed and
// unfocussable if they have no content. To remedy this, a <BR> must be // unfocusable if they have no content. To remedy this, a <BR> must be
// inserted. In Opera and IE, we just need a textnode in order for the // inserted. In Opera and IE, we just need a textnode in order for the
// cursor to appear. // cursor to appear.
var self = root.__squire__; var self = root.__squire__;
@ -2615,6 +2615,8 @@ function Squire ( root, config ) {
this.setConfig( config ); this.setConfig( config );
root.setAttribute( 'contenteditable', 'true' ); root.setAttribute( 'contenteditable', 'true' );
// Grammarly breaks the editor, *sigh*
root.setAttribute( 'data-gramm', 'false' );
// Remove Firefox's built-in controls // Remove Firefox's built-in controls
try { try {

View File

@ -64,8 +64,8 @@
return { return {
allStyles:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, allStyles:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
blankStyle:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, blankStyle:{ 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null },
colors: [ colors: [null,
"#ffebee","#ffcdd2","#ef9a9a","#e57373","#ef5350","#f44336","#e53935","#d32f2f","#c62828","#b71c1c","#fce4ec","#f8bbd0","#f48fb1","#f06292","#ec407a","#e91e63","#d81b60","#c2185b","#ad1457","#880e4f","#f3e5f5","#e1bee7","#ce93d8","#ba68c8","#ab47bc","#9c27b0","#8e24aa","#7b1fa2","#6a1b9a","#4a148c","#ede7f6","#d1c4e9","#b39ddb","#9575cd","#7e57c2","#673ab7","#5e35b1","#512da8","#4527a0","#311b92","#e8eaf6","#c5cae9","#9fa8da","#7986cb","#5c6bc0","#3f51b5","#3949ab","#303f9f","#283593","#1a237e","#e3f2fd","#bbdefb","#90caf9","#64b5f6","#42a5f5","#2196f3","#1e88e5","#1976d2","#1565c0","#0d47a1","#e1f5fe","#b3e5fc","#81d4fa","#4fc3f7","#29b6f6","#03a9f4","#039be5","#0288d1","#0277bd","#01579b","#e0f7fa","#b2ebf2","#80deea","#4dd0e1","#26c6da","#00bcd4","#00acc1","#0097a7","#00838f","#006064","#e0f2f1","#b2dfdb","#80cbc4","#4db6ac","#26a69a","#009688","#00897b","#00796b","#00695c","#004d40","#e8f5e9","#c8e6c9","#a5d6a7","#81c784","#66bb6a","#4caf50","#43a047","#388e3c","#2e7d32","#1b5e20","#f1f8e9","#dcedc8","#c5e1a5","#aed581","#9ccc65","#8bc34a","#7cb342","#689f38","#558b2f","#33691e","#f9fbe7","#f0f4c3","#e6ee9c","#dce775","#d4e157","#cddc39","#c0ca33","#afb42b","#9e9d24","#827717","#fffde7","#fff9c4","#fff59d","#fff176","#ffee58","#ffeb3b","#fdd835","#fbc02d","#f9a825","#f57f17","#fff8e1","#ffecb3","#ffe082","#ffd54f","#ffca28","#ffc107","#ffb300","#ffa000","#ff8f00","#ff6f00","#fff3e0","#ffe0b2","#ffcc80","#ffb74d","#ffa726","#ff9800","#fb8c00","#f57c00","#ef6c00","#e65100","#fbe9e7","#ffccbc","#ffab91","#ff8a65","#ff7043","#ff5722","#f4511e","#e64a19","#d84315","#bf360c","#efebe9","#d7ccc8","#bcaaa4","#a1887f","#8d6e63","#795548","#6d4c41","#5d4037","#4e342e","#3e2723","#fafafa","#f5f5f5","#eeeeee","#e0e0e0","#bdbdbd","#9e9e9e","#757575","#616161","#424242","#212121","#eceff1","#cfd8dc","#b0bec5","#90a4ae","#78909c","#607d8b","#546e7a","#455a64","#37474f","#263238","#ffffff","#000000"], 'rgb(67,67,67)','rgb(102,102,102)','rgb(153,153,153)','rgb(183,183,183)','rgb(204,204,204)','rgb(217,217,217)','rgb(239,239,239)','rgb(243,243,243)','rgb(255,255,255)','rgb(152,0,0)','rgb(255,0,0)','rgb(255,153,0)','rgb(255,255,0)','rgb(0,255,0)','rgb(0,255,255)','rgb(74,134,232)','rgb(0,0,255)','rgb(153,0,255)','rgb(255,0,255)','rgb(230,184,175)','rgb(244,204,204)','rgb(252,229,205)','rgb(255,242,204)','rgb(217,234,211)','rgb(208,224,227)','rgb(201,218,248)','rgb(207,226,243)','rgb(217,210,233)','rgb(234,209,220)','rgb(221,126,107)','rgb(234,153,153)','rgb(249,203,156)','rgb(255,229,153)','rgb(182,215,168)','rgb(162,196,201)','rgb(164,194,244)','rgb(159,197,232)','rgb(180,167,214)','rgb(213,166,189)','rgb(204,65,37)','rgb(224,102,102)','rgb(246,178,107)','rgb(255,217,102)','rgb(147,196,125)','rgb(118,165,175)','rgb(109,158,235)','rgb(111,168,220)','rgb(142,124,195)','rgb(194,123,160)','rgb(166,28,0)','rgb(204,0,0)','rgb(230,145,56)','rgb(241,194,50)','rgb(106,168,79)','rgb(69,129,142)','rgb(60,120,216)','rgb(61,133,198)','rgb(103,78,167)','rgb(166,77,121)','rgb(133,32,12)','rgb(153,0,0)','rgb(180,95,6)','rgb(191,144,0)','rgb(56,118,29)','rgb(19,79,92)','rgb(17,85,204)','rgb(11,83,148)','rgb(53,28,117)','rgb(116,27,71)','rgb(91,15,0)','rgb(102,0,0)','rgb(120,63,4)','rgb(127,96,0)','rgb(39,78,19)','rgb(12,52,61)','rgb(28,69,135)','rgb(7,55,99)','rgb(32,18,77)','rgb(76,17,48)'],
icons: ['ambulance','anchor','balance scale','bath','bed','beer','bell','bell slash','bell slash outline','bicycle','binoculars','birthday cake','blind','bomb','book','bookmark','briefcase','building','car','coffee','crosshairs','dollar sign','eye','eye slash','fighter jet','fire','fire extinguisher','flag','flag checkered','flask','gamepad','gavel','gift','glass martini','globe','graduation cap','h square','heart','heart outline','heartbeat','home','hospital','hospital outline','image','image outline','images','images outline','industry','info','info circle','key','leaf','lemon','lemon outline','life ring','life ring outline','lightbulb','lightbulb outline','location arrow','low vision','magnet','male','map','map outline','map marker','map marker alternate','map pin','map signs','medkit','money bill alternate','money bill alternate outline','motorcycle','music','newspaper','newspaper outline','paw','phone','phone square','phone volume','plane','plug','plus','plus square','plus square outline','print','recycle','road','rocket','search','search minus','search plus','ship','shopping bag','shopping basket','shopping cart','shower','street view','subway','suitcase','tag','tags','taxi','thumbtack','ticket alternate','tint','train','tree','trophy','truck','tty','umbrella','university','utensil spoon','utensils','wheelchair','wifi','wrench'] icons: ['ambulance','anchor','balance scale','bath','bed','beer','bell','bell slash','bell slash outline','bicycle','binoculars','birthday cake','blind','bomb','book','bookmark','briefcase','building','car','coffee','crosshairs','dollar sign','eye','eye slash','fighter jet','fire','fire extinguisher','flag','flag checkered','flask','gamepad','gavel','gift','glass martini','globe','graduation cap','h square','heart','heart outline','heartbeat','home','hospital','hospital outline','image','image outline','images','images outline','industry','info','info circle','key','leaf','lemon','lemon outline','life ring','life ring outline','lightbulb','lightbulb outline','location arrow','low vision','magnet','male','map','map outline','map marker','map marker alternate','map pin','map signs','medkit','money bill alternate','money bill alternate outline','motorcycle','music','newspaper','newspaper outline','paw','phone','phone square','phone volume','plane','plug','plus','plus square','plus square outline','print','recycle','road','rocket','search','search minus','search plus','ship','shopping bag','shopping basket','shopping cart','shower','street view','subway','suitcase','tag','tags','taxi','thumbtack','ticket alternate','tint','train','tree','trophy','truck','tty','umbrella','university','utensil spoon','utensils','wheelchair','wifi','wrench']
} }
}, },
@ -84,15 +84,12 @@
this.colors.forEach((color,i) => { this.colors.forEach((color,i) => {
if(i%20 <= 10){
return
}
let mod = (i % 10)+1 //1 - 10 let mod = (i % 10)+1 //1 - 10
let lines = [3, 5, 8, 9, 10] let lines = [3, 5, 8, 9, 10]
if(lines.includes(mod)){ // if(lines.includes(mod)){
reduced.push(color) reduced.push(color)
} // }
}) })
reduced.push("#000") reduced.push("#000")
@ -110,6 +107,11 @@
//Set not background to color that was chosen //Set not background to color that was chosen
this.allStyles.noteBackground = inColor this.allStyles.noteBackground = inColor
if(inColor == null){
this.$emit('changeColor', this.allStyles)
return
}
//Automatically select note text color //Automatically select note text color
// Convert hex color to RGB - http://gist.github.com/983661 // Convert hex color to RGB - http://gist.github.com/983661
@ -148,16 +150,18 @@
<style type="text/css" scoped> <style type="text/css" scoped>
.icon-button { .icon-button {
height: 40px; height: 40px;
width: 14.2%; width: calc(10% - 7px);
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
font-size: 1.3em; font-size: 1.3em;
} }
.color-button { .color-button {
height: 50px; display: inline-block;
width: 20%; width: calc(10% - 7px);
display: block; height: 30px;
border-radius: 30px;
box-shadow: 0px 1px 3px 0px #3e3e3e;
margin: 7px 7px 0 0;
cursor: pointer; cursor: pointer;
float: left;
} }
</style> </style>

View File

@ -125,7 +125,6 @@
<i class="open folder outline icon"></i> <i class="open folder outline icon"></i>
</router-link> </router-link>
</div> </div>
<div class="two wide center aligned bottom aligned column"> <div class="two wide center aligned bottom aligned column">
@ -138,7 +137,6 @@
<i class="green moon outline icon"></i> <i class="green moon outline icon"></i>
</div> </div>
<search-input v-if="loggedIn && mobile"></search-input>
<!-- mobile create note button --> <!-- mobile create note button -->
<span v-if="loggedIn"> <span v-if="loggedIn">
<span v-if="!disableNewNote" @click="createNote" class="ui large green compact icon button"> <span v-if="!disableNewNote" @click="createNote" class="ui large green compact icon button">
@ -199,11 +197,11 @@
</router-link> </router-link>
</div> </div>
<!-- <div class="menu-section" v-if="loggedIn"> <div class="menu-section" v-if="loggedIn">
<router-link v-if="loggedIn" exact-active-class="active" class="menu-item menu-button" to="/quick"> <router-link v-if="loggedIn" exact-active-class="active" class="menu-item menu-button" to="/quick">
<i class="paper plane outline icon"></i>Quick Note <i class="sticky note outline icon"></i>Scratch Pad
</router-link> </router-link>
</div> --> </div>
<div class="menu-section" v-if="!loggedIn"> <div class="menu-section" v-if="!loggedIn">
<router-link v-if="!loggedIn" class="menu-item menu-button" exact-active-class="active" to="/"> <router-link v-if="!loggedIn" class="menu-item menu-button" exact-active-class="active" to="/">
@ -217,10 +215,12 @@
<div class="menu-section"> <div class="menu-section">
<div v-on:click="toggleNightMode" class="menu-item menu-button"> <div v-on:click="toggleNightMode" class="menu-item menu-button">
<span v-if="$store.getters.getIsNightMode"> <span v-if="$store.getters.getIsNightMode == 0">
<i class="moon outline icon"></i>Black Theme</span>
<span v-if="$store.getters.getIsNightMode == 1">
<i class="moon outline icon"></i>Night Theme</span>
<span v-if="$store.getters.getIsNightMode == 2">
<i class="moon outline icon"></i>Light Theme</span> <i class="moon outline icon"></i>Light Theme</span>
<span v-else>
<i class="moon outline icon"></i>Dark Theme</span>
</div> </div>
</div> </div>
@ -257,7 +257,7 @@
}, },
data: function(){ data: function(){
return { return {
version: '2.1.0', version: '2.1.2',
username: '', username: '',
collapsed: false, collapsed: false,
mobile: false, mobile: false,
@ -329,7 +329,7 @@
.then(response => { .then(response => {
if(response.data && response.data.id){ if(response.data && response.data.id){
this.$router.push('/notes/open/'+response.data.id) // this.$router.push('/notes/open/'+response.data.id)
this.$bus.$emit('open_note', response.data.id) this.$bus.$emit('open_note', response.data.id)
this.disableNewNote = false this.disableNewNote = false
} }
@ -380,7 +380,7 @@
location.reload(true) location.reload(true)
}, },
getVersionIcon(){ getVersionIcon(){
const icons = ['cat','crow','dog','dove','dragon','fish','frog','hippo','horse','kiwi bird','otter','spider'] const icons = ['cat','crow','dog','dove','dragon','fish','frog','hippo','horse','kiwi bird','otter','spider', 'smile', 'robot', 'hat wizard', 'microchip', 'atom', 'grin tongue squint', 'radiation']
const index = ( parseInt(this.version.replace(/\./g,'')) % (icons.length)) const index = ( parseInt(this.version.replace(/\./g,'')) % (icons.length))
return icons[index] return icons[index]

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="loading-container"> <div class="loading-container">
<svg version="1.1" id="L6" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"> <svg version="1.1" id="L6" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
<rect fill="none" :stroke="$store.getters.getIsNightMode?'#FFF':'#16ab39'" stroke-width="4" x="25" y="25" width="50" height="50" rx="5"> <rect fill="none" :stroke="$store.getters.getIsNightMode > 0 ? '#FFF':'#16ab39'" stroke-width="4" x="25" y="25" width="50" height="50" rx="5">
<animateTransform <animateTransform
attributeName="transform" attributeName="transform"
dur="0.5s" dur="0.5s"
@ -12,7 +12,7 @@
attributeType="XML" attributeType="XML"
begin="rectBox.end"/> begin="rectBox.end"/>
</rect> </rect>
<rect x="25" y="25" :fill="$store.getters.getIsNightMode?'#FFF':'#16ab39'" width="50" height="50"> <rect x="25" y="25" :fill="$store.getters.getIsNightMode > 0 ? '#FFF':'#16ab39'" width="50" height="50">
<animate <animate
attributeName="height" attributeName="height"
dur="1.3s" dur="1.3s"

View File

@ -21,25 +21,25 @@
<div class="edit-divide"></div> <div class="edit-divide"></div>
<div class="edit-button" v-on:click="toggleList('ul')" data-tooltip="Task List" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="toggleList('ul')" data-tooltip="Task List" data-position="bottom center" data-inverted :class="{'edit-active':activeToDo}">
<i class="tasks icon"></i> <i class="tasks icon"></i>
</div> </div>
<div class="edit-button" v-on:click="toggleList('ol')" data-tooltip="Numbered List" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="toggleList('ol')" data-tooltip="Numbered List" data-position="bottom center" data-inverted :class="{'edit-active':activeList}">
<i class="list ol icon"></i> <i class="list ol icon"></i>
</div> </div>
<div class="edit-button" v-on:click="colorpicker = true" data-tooltip="Text Color" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="colorpicker = true" data-tooltip="Text Color" data-position="bottom center" data-inverted :style="{'color':activeColor}">
<i class="palette icon"></i> <i class="palette icon"></i>
</div> </div>
<div class="edit-button" v-on:click="toggleBold()" data-tooltip="Bold" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="toggleBold()" data-tooltip="Bold" data-position="bottom center" data-inverted :class="{'edit-active':activeBold}">
<i class="bold icon"></i> <i class="bold icon"></i>
</div> </div>
<div class="edit-button" v-on:click="toggleItalic()" data-tooltip="Quote" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="toggleItalic()" data-tooltip="Quote" data-position="bottom center" data-inverted :class="{'edit-active':activeQuote}">
<i class="quote left icon"></i> <i class="quote left icon"></i>
</div> </div>
<div class="edit-button" v-on:click="modifyFont('1.4em')" data-tooltip="Title" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="modifyFont('1.4em')" data-tooltip="Title" data-position="bottom center" data-inverted :class="{'edit-active':activeTitle}">
<i class="text height icon"></i> <i class="text height icon"></i>
</div> </div>
<div class="edit-button" v-on:click="editor.increaseQuoteLevel()" data-tooltip="Indent" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="editor.increaseQuoteLevel()" data-tooltip="Indent" data-position="bottom center" data-inverted>
@ -63,7 +63,7 @@
<div class="edit-divide"></div> <div class="edit-divide"></div>
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/colors`)" data-tooltip="Note Color" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/colors`)" data-tooltip="Note Color" data-position="bottom center" data-inverted :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
<i class="paint brush icon"></i> <i class="paint brush icon"></i>
</div> </div>
<div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/tags`)" data-tooltip="Tags" data-position="bottom center" data-inverted> <div class="edit-button" v-on:click="$router.push(`/notes/open/${noteid}/menu/tags`)" data-tooltip="Tags" data-position="bottom center" data-inverted>
@ -112,7 +112,7 @@
<div class="bottom-edit-menu"></div> <div class="bottom-edit-menu"></div>
<div class="input-container-wrapper" :class="{ 'side-menu-open':sideMenuOpen, 'size-down':(sizeDown == true)}" > <div class="input-container-wrapper" :class="{ 'side-menu-open':sideMenuOpen, 'size-down':(sizeDown == true),}" :style="{ 'background-color':styleObject['noteBackground'] }">
<!-- Squire box grows --> <!-- Squire box grows -->
<div class="note-wrapper" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}"> <div class="note-wrapper" :style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}">
@ -142,11 +142,6 @@
</div> </div>
<!-- && this.$store.getters.getIsUserOnMobile -->
<span class="note-status-indicator" v-on:click="save()" v-if="statusText != 'Saved' && $store.getters.getIsUserOnMobile">
<div class="ui green button">{{statusText}}</div>
</span>
</div> </div>
<!-- color picker --> <!-- color picker -->
@ -298,6 +293,14 @@
images: false, images: false,
options: false, options: false,
colorpicker: false, colorpicker: false,
//active button states
activeBold: false,
activeQuote: false,
activeTitle: false,
activeList: false,
activeToDo: false,
activeColor: null,
} }
}, },
watch: { watch: {
@ -383,7 +386,7 @@
this.setText(this.noteText) this.setText(this.noteText)
this.lastNoteHash = this.hashString(this.getText()) this.lastNoteHash = this.hashString(this.getText())
console.log('hash on load', this.lastNoteHash) // console.log('hash on load', this.lastNoteHash)
//focus on open, not on mobile, thats annoying //focus on open, not on mobile, thats annoying
if(!this.$store.getters.getIsUserOnMobile){ if(!this.$store.getters.getIsUserOnMobile){
@ -398,6 +401,41 @@
} }
//Change button states on editor when element is active
//eg; Bold button turns green when on bold text
this.editor.addEventListener('pathChange', e => {
//Reset all button states
this.activeBold = false
this.activeTitle = false
this.activeQuote = false
this.activeList = false
this.activeToDo = false
this.activeColor = null
let colors = e.path.match(/\d+/g)
if(colors && colors.length == 3){
this.activeColor=`rgb(${colors.join(',')})`
}
//@ TODO - Update this to match all elements, like color and bold
// index of and then the specific thing might more indexOf('B'), indexOf('I'), etc
let element = e.path.split('>').pop()
switch (element) {
case 'B': this.activeBold = true; break;
case 'I': this.activeQuote = true; break;
case 'SPAN.size[fontSize=1.4em]': this.activeTitle = true; break;
}
let parent = e.path.split('>').shift()
switch (parent) {
case 'OL': this.activeList = true; break;
case 'UL': this.activeToDo = true; break;
}
})
//Click Event - Open links when clicked in editor or toggle checks //Click Event - Open links when clicked in editor or toggle checks
this.editor.addEventListener('click', e => { this.editor.addEventListener('click', e => {
@ -1003,17 +1041,17 @@
this.statusText = '' this.statusText = ''
this.diffText() // this.diffText()
//Each note, save after 5 seconds, focus lost or 30 characters typed. //Each note, save after 15 seconds, focus lost or 30 characters typed.
clearTimeout(this.editDebounce) clearTimeout(this.editDebounce)
this.editDebounce = setTimeout(() => { this.editDebounce = setTimeout(() => {
this.save() this.save()
}, 5000) }, 15 * 1000)
//Save after 30 keystrokes //Save after 50 keystrokes
this.keyPressesCounter = (this.keyPressesCounter + 1) this.keyPressesCounter = (this.keyPressesCounter + 1)
if(this.keyPressesCounter > 30){ if(this.keyPressesCounter > 50){
this.keyPressesCounter = 0 this.keyPressesCounter = 0
this.save() this.save()
} }
@ -1048,7 +1086,7 @@
'hash': currentHash, 'hash': currentHash,
} }
console.log('Save Hash', currentHash) // console.log('Save Hash', currentHash)
this.statusText = 'Saving' this.statusText = 'Saving'
axios.post('/api/note/update', postData).then( response => { axios.post('/api/note/update', postData).then( response => {
@ -1148,8 +1186,8 @@
setupWebSockets(){ setupWebSockets(){
this.$io.on('new_note_text_saved', ({noteId, hash}) => { this.$io.on('new_note_text_saved', ({noteId, hash}) => {
console.log('Current hash', this.lastNoteHash) // console.log('Current hash', this.lastNoteHash)
console.log('Incoming Hash', hash) // console.log('Incoming Hash', hash)
}) })
return return
@ -1255,6 +1293,10 @@
.edit-button:hover { .edit-button:hover {
background-color: var(--menu-accent); background-color: var(--menu-accent);
} }
.edit-active {
background-color: #21BA45;
color: white;
}
.edit-divide { .edit-divide {
display: inline-block; display: inline-block;
background-color: var(--menu-accent); background-color: var(--menu-accent);
@ -1313,7 +1355,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 300px; min-height: 300px;
background: var(--background_color); /*background: var(--background_color);*/
/*opacity: 0.;*/ /*opacity: 0.;*/
z-index: 1; z-index: 1;
} }

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="note-title-display-card" <div class="note-title-display-card"
:style="{'background-color':color, 'color':fontColor, 'border-color':color }" :style="{'background-color':color, 'color':fontColor, 'border-color':color }"
:class="{'currently-open':currentlyOpen, 'bgboy':triggerClosedAnimation}" :class="{'currently-open':currentlyOpen, 'bgboy':triggerClosedAnimation, 'title-view':titleView }"
> >
<!-- Show title and snippet below it --> <!-- Show title and snippet below it -->
<div class="overflow-hidden note-card-text" @click="cardClicked"> <div class="overflow-hidden note-card-text" @click="cardClicked" v-if="!titleView">
<span class="subtext" v-if="note.shareUsername"> <span class="subtext" v-if="note.shareUsername">
Shared by {{ note.shareUsername }} Shared by {{ note.shareUsername }}
@ -62,13 +62,17 @@
</div> </div>
<div v-if="titleView" class="single-line-text" @click="cardClicked">
<span class="title-line" v-if="note.title.length > 0">{{ note.title }}<br></span>
<span class="sub-line" v-if="note.subtext.length > 0">{{ removeHtml(note.subtext) }}</span>
<span v-if="note.title.length == 0 && note.title.length == 0">Empty Note</span>
</div>
<!-- Toolbar on the bottom --> <!-- Toolbar on the bottom -->
<div class="tool-bar" @click.self="cardClicked"> <div class="tool-bar" @click.self="cardClicked" v-if="!titleView">
<div class="icon-bar"> <div class="icon-bar">
<!-- <span v-if="note.pinned == 1" data-position="top left" data-tooltip="Pinned" data-inverted> <!-- <span v-if="note.pinned == 1" data-position="top left" data-tooltip="Pinned" data-inverted>
<i class="green pin icon"></i> <i class="green pin icon"></i>
</span> </span>
<span v-if="note.archived == 1" data-position="top left" data-tooltip="Archived" data-inverted> <span v-if="note.archived == 1" data-position="top left" data-tooltip="Archived" data-inverted>
@ -113,9 +117,8 @@
<img v-for="thumb in getThumbs" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`"> <img v-for="thumb in getThumbs" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`">
</div> </div>
</div> </div>
</div> </div>
<side-slide-menu v-if="showTagSlideMenu" v-on:close="toggleTags(false)" :full-shadow="true" :skip-history="true"> <side-slide-menu v-if="showTagSlideMenu" v-on:close="toggleTags(false)" :full-shadow="true" :skip-history="true">
<div class="ui basic segment"> <div class="ui basic segment">
<note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/> <note-tag-edit :noteId="note.id" :key="'display-tags-for-note-'+note.id"/>
@ -136,7 +139,7 @@
export default { export default {
name: 'NoteTitleDisplayCard', name: 'NoteTitleDisplayCard',
props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults' ], props: [ 'onClick', 'data', 'currentlyOpen', 'textResults', 'attachmentResults', 'tagResults', 'titleView' ],
components: { components: {
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default, 'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
'note-tag-edit': require('@/components/NoteTagEdit.vue').default, 'note-tag-edit': require('@/components/NoteTagEdit.vue').default,
@ -148,6 +151,17 @@
this.beenClicked = true this.beenClicked = true
this.onClick(this.note.id) this.onClick(this.note.id)
}, },
removeHtml(string){
if(string == undefined || string == null || string.length == 0){
return ''
}
return string
.replace(/&[[#A-Za-z0-9]+A-Za-z0-9]+;/g,' ') //Rip out all HTML entities
.replace(/<[^>]+>/g, ' ') //Rip out all HTML tags
.replace(/\s+/g, ' ') //Remove all whitespace
.trim()
},
cleanHighlight(text){ cleanHighlight(text){
//Basically just remove whitespace //Basically just remove whitespace
let updated = text.replace(/&nbsp;/g, '').replace(/<br>/g,'') let updated = text.replace(/&nbsp;/g, '').replace(/<br>/g,'')
@ -395,6 +409,27 @@
.note-title-display-card:hover { .note-title-display-card:hover {
box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8); box-shadow: 0px 2px 2px 1px rgba(210, 211, 211, 0.8);
} }
.note-title-display-card.title-view {
width: 100%;
min-height: 10px;
max-width: none;
box-shadow: 0px 0px 1px 1px rgba(210, 211, 211, 0.46);
}
.single-line-text {
width: calc(100% - 25px);
margin: 5px 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
}
.title-line {
font-weight: bold;
font-size: 1.2em;
padding: 0 20px 0 0;
}
.icon-bar { .icon-bar {
display: inline-block; display: inline-block;
padding: 5px 10px 0; padding: 5px 10px 0;

View File

@ -8,45 +8,69 @@
} }
.floating-button { .floating-button {
position: absolute; position: absolute;
right: 0; right: 7px;
top: 4px; top: 4px;
z-index: 2; z-index: 2;
} }
.floating-note-options {
position: absolute;
right: 0;
left: 0;
top: 35px;
z-index: 2;
}
.floating-note-options > .segment {
border-top: none;
border-top-right-radius: 0;
border-top-left-radius: 0;
}
</style> </style>
<template> <template>
<span> <span v-on:mouseenter="extraMenuHover = true" v-on:mouseleave="extraMenuHover = false">
<div class="ui form" v-if="!$store.getters.getIsUserOnMobile"> <div class="ui form">
<!-- normal search menu --> <!-- normal search menu -->
<div class="ui left icon fluid input"> <div class="ui left icon fluid input">
<input ref="desktopSearch" v-model="searchTerm" @keyup.enter="search" placeholder="Search Notes and Files" /> <input ref="desktopSearch" v-on:blur="focused = false" v-on:focus="focused = true" v-model="searchTerm" @keydown="onKeyDown" @keyup="onKeyUp" placeholder="Search or Start Typing New Note" />
<i class="search icon"></i> <i class="search icon"></i>
</div> </div>
<div class="floating-button" v-if="searchTerm.length > 0 && !searched">
<div class="floating-button" v-if="searchTerm.length > 0 && !searched && searchTerm.indexOf(' ') == -1">
<div class="ui green compact button" v-on:click="search()">Search</div> <div class="ui green compact button" v-on:click="search()">Search</div>
</div> </div>
<div class="floating-button" v-if="searchTerm.length > 0 && searched"> <div class="floating-button" v-if="searchTerm.length > 0 && searched">
<div class="ui grey compact button" v-on:click="clear()">Clear</div> <div class="ui grey compact button" v-on:click="clear()">Clear</div>
</div> </div>
</div>
<!-- Only show button on mobile --> <div class="floating-button" v-if="!searched && searchTerm.length > 0 && searchTerm.indexOf(' ') != -1">
<span class="ui basic icon button" v-on:click="openFloatingSearch" v-if="$store.getters.getIsUserOnMobile"> <div class="ui grey compact button" v-on:click="searchTerm = ''">Clear</div>
<i class="green search icon"></i> </div>
</span>
<div class="fixed-search" v-if="showFixedSearch"> <div class="floating-note-options"
<div class="ui raised segment"> v-if="(searchTerm.indexOf(' ') != -1 || tagSuggestions.length > 0) && (extraMenuHover)">
<h2 class="ui center aligned header">Search!</h2> <div class="ui segment">
<div class="ui form"> <div class="ui very compact grid" v-if="searchTerm.indexOf(' ') != -1">
<div class="ui left icon fluid input"> <div class="eight wide column">
<input <div class="ui green compact shrinking button" v-on:click="pushToNewNote()">
ref="fixedSearch" <i class="plus icon"></i>A New Note
v-model="searchTerm" </div>
@keyup.enter="search" </div>
v-on:blur="showFixedSearch = false" <div class="eight wide right aligned column">
placeholder="Press Enter to Search" /> <div class="ui green compact shrinking button" v-on:click="pushToQuickNote()">
<i class="search icon"></i> <i class="sticky note outline icon"></i>The Scratch Pad
</div>
</div>
</div>
<div class="ui very compact grid" v-if="tagSuggestions.length > 0">
<div class="sixteen wide column">
<div class="ui clickable green label" v-for="tag in tagSuggestions" v-on:click="tagClick(tag.id)">
<i class="tag icon"></i>
{{ tag.text }}
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -57,13 +81,19 @@
<script> <script>
import axios from 'axios'
export default { export default {
data: function(){ data: function(){
return { return {
searchTerm: '', searchTerm:'',
showFixedSearch: false,
searched: false, searched: false,
tagSuggestions: [],
tagSearchDebounce: null,
extraMenuHover: false,
} }
}, },
beforeCreate: function(){ beforeCreate: function(){
@ -73,36 +103,105 @@
//search clear //search clear
this.$bus.$on('reset_fast_filters', () => { this.$bus.$on('reset_fast_filters', () => {
this.searchTerm = '' this.searchTerm = ''
this.tagSuggestions = []
}) })
}, },
methods: { methods: {
openFloatingSearch(){ tagClick(tagId){
this.showFixedSearch = !this.showFixedSearch
this.$emit('tagClick', tagId)
this.tagSuggestions = []
this.searchTerm = ''
if(this.showFixedSearch){
this.$nextTick( () => {
this.searchTerm = ''
this.$refs.fixedSearch.focus()
})
}
}, },
clear(){ clear(){
this.searched = false this.searched = false
this.searchTerm = '' this.searchTerm = ''
this.tagSuggestions = []
if(!this.$store.getters.getIsUserOnMobile){ if(!this.$store.getters.getIsUserOnMobile){
this.$refs.desktopSearch.focus() this.$refs.desktopSearch.focus()
} }
this.$bus.$emit('note_reload') this.$bus.$emit('note_reload')
}, },
pushToQuickNote(){
const text = this.searchTerm
this.searchTerm = ''
this.tagSuggestions = []
axios.post('/api/quick-note/update', { 'pushText':text.trim() } )
.then( response => {
//Open Quick Note
if(response.data && response.data.id){
this.$router.push('/notes/open/'+response.data.id)
this.$bus.$emit('open_note', response.data.id)
}
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Update The Scratch Pad') })
},
pushToNewNote(){
const text = this.searchTerm
this.searchTerm = ''
this.tagSuggestions = []
axios.post('/api/note/create', { text })
.then(response => {
if(response.data && response.data.id){
this.$router.push('/notes/open/'+response.data.id)
this.$bus.$emit('open_note', response.data.id)
}
})
.catch(error => { this.$bus.$emit('notification', 'Failed to create note') })
},
onKeyUp(event){
//Search Tags
const postData = {
'tagText':this.searchTerm.trim()
}
clearTimeout(this.tagSearchDebounce)
if(this.searchTerm.length == 0){
this.tagSuggestions = []
return
}
this.tagSearchDebounce = setTimeout(() => {
this.tagSuggestions = []
axios.post('/api/tag/suggest', postData)
.then( response => {
this.tagSuggestions = response.data
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Get Suggested Tags') })
}, 800)
},
onKeyDown(event){
//Commant + Enter
if((event.metaKey || event.ctrlKey) && event.keyCode == 13 ){
this.pushToQuickNote()
return
}
if(event.keyCode == 13){
this.search()
return
}
},
search(){ search(){
this.searched = true this.searched = true
this.$refs.desktopSearch.focus()
//Blur after search on mobile
if(this.$store.getters.getIsUserOnMobile){ if(this.$store.getters.getIsUserOnMobile){
this.$refs.fixedSearch.blur() this.$refs.desktopSearch.blur()
}
if(!this.$store.getters.getIsUserOnMobile){
this.$refs.desktopSearch.focus()
} }
this.$bus.$emit('update_search_term', this.searchTerm) this.$bus.$emit('update_search_term', this.searchTerm)
}, },
} }

View File

@ -22,6 +22,14 @@
<script> <script>
export default { export default {
name: 'HelpPage' name: 'HelpPage',
props:[ 'message' ],
data () {
return {
items: []
}
},
methods: {
}
} }
</script> </script>

View File

@ -31,10 +31,16 @@
v-on:tagClick="tagId => toggleTagFilter(tagId)" v-on:tagClick="tagId => toggleTagFilter(tagId)"
/> />
<div class="ui basic shrinking icon button" v-on:click="toggleTitleView()">
<i v-if="titleView" class="th icon"></i>
<i v-if="!titleView" class="bars icon"></i>
</div>
</div> </div>
<div class="six wide column" v-if="!$store.getters.getIsUserOnMobile"> <div class="six wide column">
<search-input <search-input
v-on:tagClick="tagId => toggleTagFilter(tagId)"
v-if="$store.getters.totals && $store.getters.totals['totalNotes']" /> v-if="$store.getters.totals && $store.getters.totals['totalNotes']" />
</div> </div>
@ -49,9 +55,16 @@
</div> </div>
<h2 v-if="searchTerm.length > 0 && !loadingInProgress"> <div class="sixteen wide column" v-if="searchTerm.length > 0 && !loadingInProgress">
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}" <h2 class="ui header">
</h2> <div class="content">
{{ searchResultsCount.toLocaleString() }} notes with keyword "{{ searchTerm }}"
<div v-if="searchResultsCount == 0" class="sub header">
Search can only find key words. Try a single word search.
</div>
</div>
</h2>
</div>
<h2 v-if="fastFilters['withLinks'] == 1">Notes with Links</h2> <h2 v-if="fastFilters['withLinks'] == 1">Notes with Links</h2>
<h2 v-if="fastFilters['withTags'] == 1">Notes with Tags</h2> <h2 v-if="fastFilters['withTags'] == 1">Notes with Tags</h2>
@ -81,6 +94,7 @@
:ref="'note-'+note.id" :ref="'note-'+note.id"
:onClick="openNote" :onClick="openNote"
:data="note" :data="note"
:title-view="titleView"
:currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)" :currently-open="(activeNoteId1 == note.id || activeNoteId2 == note.id)"
:key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated" :key="note.id + note.color + note.note_highlights.length + note.attachment_highlights.length + ' -' + note.tag_highlights.length + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
/> />
@ -152,6 +166,7 @@
highlights: [], highlights: [],
searchDebounce: null, searchDebounce: null,
fastFilters: {}, fastFilters: {},
titleView: false,
//Load up notes in batches //Load up notes in batches
firstLoadBatchSize: 10, //First set of rapidly loaded notes firstLoadBatchSize: 10, //First set of rapidly loaded notes
@ -207,10 +222,18 @@
this.$parent.loginGateway() this.$parent.loginGateway()
this.$io.on('new_note_created', noteId => {
//Do not update note if its open
if(this.activeNoteId1 != noteId){
this.updateSingleNote(parseInt(noteId))
}
})
//Update title cards when new note text is saved
this.$io.on('new_note_text_saved', ({noteId, hash}) => { this.$io.on('new_note_text_saved', ({noteId, hash}) => {
//Do not update note if its open //Do not update note if its open
if(this.activeNoteId1 != noteId){ if(this.activeNoteId1 != noteId){
console.log('notePage: update display of note ', noteId) this.updateSingleNote(parseInt(noteId))
} }
}) })
@ -237,6 +260,7 @@
this.noteSections[key].forEach( (note, index) => { this.noteSections[key].forEach( (note, index) => {
if(note.id == noteId){ if(note.id == noteId){
this.noteSections[key].splice(index,1) this.noteSections[key].splice(index,1)
this.$store.dispatch('fetchAndUpdateUserTotals')
return return
} }
}) })
@ -312,6 +336,9 @@
this.reset() this.reset()
}, },
methods: { methods: {
toggleTitleView(){
this.titleView = !this.titleView
},
showOneColumn(){ showOneColumn(){
return this.$store.getters.getIsUserOnMobile return this.$store.getters.getIsUserOnMobile
@ -328,32 +355,13 @@
if(nodeClick == 'A'){ return } if(nodeClick == 'A'){ return }
} }
//Do not open same note twice
if(this.activeNoteId1 == id || this.activeNoteId2 == id){
return;
}
//1 note open //1 note open
if(this.activeNoteId1 == null && this.activeNoteId2 == null){ if(this.activeNoteId1 == null){
this.activeNoteId1 = id this.activeNoteId1 = id
this.activeNote1Position = 0 //Middel of page this.activeNote1Position = 0 //Middel of page
this.$router.push('/notes/open/'+this.activeNoteId1) this.$router.push('/notes/open/'+this.activeNoteId1)
return return
} }
//2 notes open
if(this.activeNoteId1 != null && this.activeNoteId2 == null){
this.activeNoteId2 = id
this.activeNote1Position = 1 //Right side of page
this.activeNote2Position = 2 //Left side of page
return
}
//2 notes open
if(this.activeNoteId2 != null && this.activeNoteId1 == null){
this.activeNoteId1 = id
this.activeNote1Position = 2 //Right side of page
this.activeNote2Position = 1 //Left side of page
return
}
}, },
closeNote(position){ closeNote(position){
//One note open, close that note //One note open, close that note
@ -448,10 +456,6 @@
if(this.$refs.note1 && this.$refs.note1.currentNoteId == noteIdToClose){ if(this.$refs.note1 && this.$refs.note1.currentNoteId == noteIdToClose){
// this.$refs.note1.close() // this.$refs.note1.close()
} }
if(this.$refs.note2 && this.$refs.note2.currentNoteId == noteIdToClose){
//this.$refs.note2.close()
}
} }
}, },
visibiltyChangeAction(event){ visibiltyChangeAction(event){
@ -475,6 +479,9 @@
// @TODO Don't even trigger this if the note wasn't changed // @TODO Don't even trigger this if the note wasn't changed
updateSingleNote(noteId){ updateSingleNote(noteId){
//Find local note, if it exists; continue
//Lookup one note using passed in ID //Lookup one note using passed in ID
const postData = { const postData = {
searchQuery: this.searchTerm, searchQuery: this.searchTerm,
@ -484,8 +491,7 @@
} }
} }
//Note data must be fetched, then sorted into existing note data
axios.post('/api/note/search', postData) axios.post('/api/note/search', postData)
.then(results => { .then(results => {
@ -526,6 +532,7 @@
this.$nextTick( () => { this.$nextTick( () => {
//Trigger close animation on note
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){ if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].justClosed() this.$refs['note-'+noteId][0].justClosed()
} }

View File

@ -4,31 +4,27 @@
<div class="ui sixteen wide column"> <div class="ui sixteen wide column">
<h2 class="ui header"> <h2 class="ui header">
<i class="paper plane outline icon"></i> <i class="sticky note outline icon"></i>
<div class="content"> <div class="content">
Quick The Scratch Pad
<div class="sub header">Rapidly save text</div> <div class="sub header">One place to put random junk</div>
</div> </div>
</h2> </h2>
</div> </div>
<div class="sixteen wide middle aligned column"> <div class="sixteen wide middle aligned column">
<div class="ui compact basic button" <div class="ui compact basic right floated button shrinking" v-if="!showNewNoteConfirm" v-on:click="showNewNoteConfirm = true">
v-on:click="enterToSubmit = !enterToSubmit"> <i class="sync alternate reload icon"></i>
<i v-if="enterToSubmit" class="green toggle on icon"></i> New Quick Note
<i v-else class="toggle off icon"></i>
<span v-if="enterToSubmit">Save after Enter press</span>
<span v-else>CTRL + Enter to Save</span>
</div> </div>
<div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="showNewNoteConfirm = false">
<div class="ui compact basic button" <i class="close icon"></i>
v-on:click="pasteToSubmit = !pasteToSubmit"> Cancel
<i v-if="pasteToSubmit" class="green check circle outline icon"></i> </div>
<i v-else class="circle outline icon"></i> <div v-if="showNewNoteConfirm" class="ui compact basic right floated button shrinking" v-on:click="newQuickNote()">
Save after Pasting <i class="green thumbs up icon"></i>
Confirm
</div> </div>
</div> </div>
@ -41,25 +37,28 @@
ref="fastInput" ref="fastInput"
v-model="newText" v-model="newText"
v-on:keydown="checkKeyup" v-on:keydown="checkKeyup"
v-on:paste="onPaste"
placeholder="Push to the top of the quick note." placeholder="Push to the top of the quick note."
></textarea> ></textarea>
</div> </div>
<div class="field"> <div class="field">
<div v-on:click="appendQuickNote" class="ui green button">Save</div> <div v-on:click="appendQuickNote" class="ui green button">Save (Enter)</div>
<div v-if="quickNoteId" class="ui right floated basic button" v-on:click="$router.push('/attachments/note/'+quickNoteId)"> <div v-if="quickNoteId" class="ui right floated basic button" v-on:click="$router.push('/attachments/note/'+quickNoteId)">
<i class="folder open outline icon"></i> <i class="folder open outline icon"></i>
Files Files
</div> </div>
<div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button"> <div v-if="quickNoteId" v-on:click="openNoteEdit" class="ui right floated basic button">
<i class="file outline icon"></i> <i class="file outline icon"></i>
Edit Open Note
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="fun" v-html="savedQuickNoteText"></div> <div class="one wide column"></div>
<div class="fourteen wide column">
<div class="note-card-text" v-html="savedQuickNoteText"></div>
</div>
<div class="one wide column"></div>
</div> </div>
</div> </div>
</template> </template>
@ -79,8 +78,7 @@
newText: '', newText: '',
savedQuickNoteText: '', savedQuickNoteText: '',
quickNoteId: null, quickNoteId: null,
pasteToSubmit: true, showNewNoteConfirm: false,
enterToSubmit: true,
} }
}, },
beforeCreate: function(){ beforeCreate: function(){
@ -89,6 +87,16 @@
// //
this.$parent.loginGateway() this.$parent.loginGateway()
}, },
beforeMount(){
this.$io.on('new_note_created', noteId => {
this.getQuickNote()
})
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
this.getQuickNote()
})
},
mounted: function(){ mounted: function(){
if(this.$refs.fastInput){ if(this.$refs.fastInput){
@ -107,6 +115,17 @@
} }
}, },
methods: { methods: {
newQuickNote(){
this.showNewNoteConfirm = false
axios.post('/api/quick-note/new')
.then( ({data}) => {
this.savedQuickNoteText = ''
this.quickNoteId = ''
})
},
openNoteEdit(){ openNoteEdit(){
this.$router.push({'path':'/notes/open/'+this.quickNoteId}) this.$router.push({'path':'/notes/open/'+this.quickNoteId})
}, },
@ -119,17 +138,10 @@
element.style.height = (element.scrollHeight + padding) +'px'; element.style.height = (element.scrollHeight + padding) +'px';
//Enter Key submits by default //Enter Key submits by default
if(event.keyCode == 13 && this.enterToSubmit == true){ if(event.keyCode == 13){
this.appendQuickNote() this.appendQuickNote()
return return
} }
//Alternate submit
//If command+enter or control+enter is pressed, submit
if((event.metaKey || event.ctrlKey) && [13].includes(event.keyCode) && this.enterToSubmit == false){
this.appendQuickNote()
return
}
}, },
appendQuickNote(){ appendQuickNote(){
@ -142,28 +154,17 @@
this.newText = '' //Clear text area this.newText = '' //Clear text area
this.$refs.fastInput.style.height = 'auto' //Back to normal size this.$refs.fastInput.style.height = 'auto' //Back to normal size
this.savedQuickNoteText = results.data.text
this.quickNoteId = results.data.id
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to Update Quick Note') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Update Quick Note') })
}, },
getQuickNote (){ getQuickNote(){
axios.post('/api/quick-note/get') axios.post('/api/quick-note/get')
.then( results => { .then( ({data}) => {
this.savedQuickNoteText = results.data.text this.savedQuickNoteText = data.text
this.quickNoteId = results.data.id this.quickNoteId = data.id
}) })
.catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Quick Note') }) .catch(error => { this.$bus.$emit('notification', 'Failed to Fetch Quick Note') })
}, },
onPaste(event){
if(this.pasteToSubmit == true){
setTimeout( () => {
this.appendQuickNote()
}, 10)
}
return true
},
} }
} }
</script> </script>

View File

@ -60,7 +60,7 @@ export default new Router({
{ {
path: '/quick', path: '/quick',
name: 'Quick', name: 'Quick',
meta: {title:'Quick'}, meta: {title:'Scratch Pad'},
component: QuickPage component: QuickPage
}, },
{ {

View File

@ -41,38 +41,61 @@ export default new Vuex.Store({
state.token = null state.token = null
state.username = null state.username = null
}, },
toggleNightMode(state){ toggleNightMode(state, pastTheme){
//Toggle state and save to local storage const themes = {
state.nightMode = !(state.nightMode) 'white':{
localStorage.setItem('nightMode', state.nightMode) 'background_color': '#fff',
'text_color': '#3d3d3d',
//Default theme colors 'outline_color': 'rgba(34,36,38,0.15)',
let themeColors = { 'border_color': 'rgba(34,36,38,0.20)',
'background_color': '#fff', 'menu-accent': '#cecece',
'text_color': '#3d3d3d', 'menu-text': '#5e6268',
'outline_color': 'rgba(34,36,38,0.15)', },
'border_color': 'rgba(34,36,38,0.20)', 'black':{
'menu-accent': '#cecece', 'background_color': '#000',
'menu-text': '#5e6268', 'text_color': '#FFF',
} 'outline_color': '#FFF',
//Night mode colors 'border_color': 'rgba(255, 255, 255, 0.70)',
if(state.nightMode){ 'menu-accent': '#626262',
themeColors = { 'menu-text': '#d9d9d9',
},
'night':{
'background_color': '#000', 'background_color': '#000',
'text_color': '#a98457', 'text_color': '#a98457',
'outline_color': '#a98457', 'outline_color': '#a98457',
'border_color': 'rgba(255, 255, 255, 0.31)', 'border_color': 'rgba(255, 255, 255, 0.31)',
'menu-accent': '#626262', 'menu-accent': '#626262',
'menu-text': '#d9d9d9', 'menu-text': '#d9d9d9',
} },
} }
//Catch values not in set
const totalThemes = Object.keys(themes).length
state.nightMode++
if(state.nightMode > totalThemes-1){
state.nightMode = 0
}
if(pastTheme != null){
state.nightMode = pastTheme
}
//Final catch for numbers
if(Number.isInteger(parseInt(state.nightMode)) == false){
state.nightMode = 0
}
const currentTheme = Object.keys(themes)[state.nightMode]
//Toggle state and save to local storage
localStorage.setItem('nightMode', state.nightMode)
//Go through each color and set CSS variable //Go through each color and set CSS variable
let root = document.documentElement let root = document.documentElement
Object.keys(themeColors).forEach( attribute => { Object.keys( themes[currentTheme] ).forEach( attribute => {
root.style.setProperty('--'+attribute, themeColors[attribute]) root.style.setProperty('--'+attribute, themes[currentTheme][attribute])
}) })
}, },
detectIsUserOnMobile(state){ detectIsUserOnMobile(state){

View File

@ -139,8 +139,8 @@ app.use(function(req, res, next){
// Test Area // Test Area
// -> right here // -> right here
let UserTest = require('@models/User') // let UserTest = require('@models/User')
let NoteTest = require('@models/Note') // let NoteTest = require('@models/Note')
// UserTest.keyPairTest() // UserTest.keyPairTest()
// .then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey)) // .then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey))
// .then( message => { console.log(message) }) // .then( message => { console.log(message) })

View File

@ -24,7 +24,7 @@ Note.test = (userId, masterKey) => {
let testNoteId = 0 let testNoteId = 0
Note.create(userId, '','', masterKey) Note.create(null, userId, '','', masterKey)
.then(newNoteId => { .then(newNoteId => {
console.log('Test: Create Note - Pass') console.log('Test: Create Note - Pass')
@ -164,7 +164,7 @@ Note.encryptEveryNote = (userId, masterKey) => {
} }
//Returns insertedId of new note //Returns insertedId of new note
Note.create = (userId, noteTitle, noteText, masterKey) => { Note.create = (io, userId, noteTitle = '', noteText = '', masterKey) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if(userId == null || userId < 10){ reject('User Id required to create note') } if(userId == null || userId < 10){ reject('User Id required to create note') }
@ -173,6 +173,9 @@ Note.create = (userId, noteTitle, noteText, masterKey) => {
const salt = cs.createSmallSalt() const salt = cs.createSmallSalt()
const snippetSalt = cs.createSmallSalt() const snippetSalt = cs.createSmallSalt()
const snippetObj = JSON.stringify([noteTitle, noteText.substring(0, 500)])
const snippet = cs.encrypt(masterKey, snippetSalt, snippetObj)
const textObject = JSON.stringify([noteTitle, noteText]) const textObject = JSON.stringify([noteTitle, noteText])
const encryptedText = cs.encrypt(masterKey, salt, textObject) const encryptedText = cs.encrypt(masterKey, salt, textObject)
@ -183,10 +186,15 @@ Note.create = (userId, noteTitle, noteText, masterKey) => {
const rawTextId = rows[0].insertId const rawTextId = rows[0].insertId
return db.promise() return db.promise()
.query('INSERT INTO note (user_id, note_raw_text_id, created, quick_note, snippet_salt) VALUES (?,?,?,?,?)', .query('INSERT INTO note (user_id, note_raw_text_id, created, quick_note, snippet, snippet_salt) VALUES (?,?,?,?,?,?)',
[userId, rawTextId, created, 0, snippetSalt]) [userId, rawTextId, created, 0, snippet, snippetSalt])
}) })
.then((rows, fields) => { .then((rows, fields) => {
if(io){
io.to(userId).emit('new_note_created', rows[0].insertId)
}
// Indexing is done on save // Indexing is done on save
resolve(rows[0].insertId) //Only return the new note ID when creating a new note resolve(rows[0].insertId) //Only return the new note ID when creating a new note
}) })

View File

@ -5,45 +5,76 @@ let Note = require('@models/Note')
let QuickNote = module.exports = {} let QuickNote = module.exports = {}
QuickNote.get = (userId) => { QuickNote.get = (userId, masterKey) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise() db.promise()
.query(` .query(`
SELECT note.id, text FROM note SELECT note.id FROM note WHERE quick_note = 1 AND user_id = ? LIMIT 1
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE quick_note = 1 AND user_id = ? LIMIT 1
`, [userId]) `, [userId])
.then((rows, fields) => { .then((rows, fields) => {
//Quick Note is set, return note text //Quick Note is set, return note text
if(rows[0].length == 1){ if(rows[0][0] != undefined){
resolve({ let noteId = rows[0][0].id
id: rows[0][0].id, Note.get(userId, noteId, masterKey)
text: rows[0][0].text .then( noteObject => {
return resolve(noteObject)
}) })
} else {
return resolve(null)
} }
resolve({
id: null,
text: 'Enter something to create a quick note.'
})
}) })
.catch(console.log) .catch(console.log)
}) })
} }
QuickNote.update = (userId, pushText) => { QuickNote.newNote = (userId) => {
return new Promise((resolve, reject) => {
db.promise().query('UPDATE note SET quick_note = 0 WHERE quick_note = 1 AND user_id = ?',[userId])
.then((rows, fields) => {
resolve(true)
})
})
}
QuickNote.makeUrlLink = (inputText) => {
var replacedText, replacePattern1, replacePattern2, replacePattern3;
//URLs starting with http://, https://, or ftp://
replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
replacedText = inputText.replace(replacePattern1, '<a href="$1" target="_blank">$1</a>');
//URLs starting with "www." (without // before it, or it'd re-link the ones done above).
replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
//Change email addresses to mailto:: links.
replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
return replacedText;
}
QuickNote.update = (io, userId, pushText, masterKey) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let finalId = null
let finalText = ''
//Process pushText, split at \n (new lines), put <p> tags around each new line //Process pushText, split at \n (new lines), put <p> tags around each new line
let broken = '<blockquote>' + let broken = '<p>' +
pushText.split(/\r?\n/).map( (line, index) => { pushText.split(/\r?\n/).map( (line, index) => {
let clean = line let clean = line
.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
//Turn links into actual linx
clean = QuickNote.makeUrlLink(clean)
if(clean == ''){ clean = '&nbsp;' } if(clean == ''){ clean = '&nbsp;' }
let newLine = '' let newLine = ''
if(index > 0){ newLine = '<br>' } if(index > 0){ newLine = '<br>' }
@ -51,51 +82,31 @@ QuickNote.update = (userId, pushText) => {
//Return line wrapped in p tags //Return line wrapped in p tags
return `${newLine}<span>${clean}</span>` return `${newLine}<span>${clean}</span>`
}).join('') + '</blockquote>' }).join('') + '</p><p><br></p>'
db.promise() QuickNote.get(userId, masterKey)
.query(` .then(noteObject => {
SELECT note.id, text, color, pinned, archived
FROM note
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
WHERE quick_note = 1 AND user_id = ? LIMIT 1
`, [userId])
.then((rows, fields) => {
//Quick Note is set, push it the front of existing note if(noteObject == null){
if(rows[0].length == 1){
let d = rows[0][0] //Get row data finalText += broken
//Push old text behind fresh new text return Note.create(io, userId, 'Quick Note', finalText, masterKey)
let newText = broken +''+ d.text .then(insertedId => {
finalId = insertedId
//Save that, then return the new text return db.promise().query('UPDATE note SET quick_note = 1 WHERE id = ? AND user_id = ?',[insertedId, userId])
Note.update(null, userId, d.id, newText, '', d.color, d.pinned, d.archived)
.then( saveResults => {
resolve({
id:d.id,
text:newText
})
}) })
} else { } else {
//Create a new note with the quick text submitted. finalText += (broken + noteObject.text)
Note.create(userId, broken, 1) finalId = noteObject.id
.then( insertId => { return Note.update(io, userId, noteObject.id, finalText, noteObject.title, noteObject.color, noteObject.pinned, noteObject.archived, null, masterKey)
resolve({
id:insertId,
text:broken
})
})
} }
}) })
.catch(console.log) .then( saveResults => {
return resolve(true)
})
}) })
//Lookup quick note,
//Note.create(userId, 'Quick Note', 1)
} }

View File

@ -176,16 +176,24 @@ Tag.suggest = (userId, noteId, tagText) => {
tagText += '%' tagText += '%'
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.promise()
.query(`SELECT text FROM note_tag let params = [userId, tagText]
let query = `SELECT tag.id, text FROM note_tag
JOIN tag ON note_tag.tag_id = tag.id JOIN tag ON note_tag.tag_id = tag.id
WHERE note_tag.user_id = ? WHERE note_tag.user_id = ?
AND tag.text LIKE ? AND tag.text LIKE ?`
AND note_tag.tag_id NOT IN (
SELECT note_tag.tag_id FROM note_tag WHERE note_tag.note_id = ? if(noteId && noteId > 0){
) params.push(noteId)
GROUP BY text query += `AND note_tag.tag_id NOT IN
LIMIT 6`, [userId, tagText, noteId]) (SELECT note_tag.tag_id FROM note_tag WHERE note_tag.note_id = ?)`
}
query += ` GROUP BY text, id LIMIT 6`
db.promise()
.query(query, params)
.then((rows, fields) => { .then((rows, fields) => {
resolve(rows[0]) //Return new ID resolve(rows[0]) //Return new ID
}) })

View File

@ -34,7 +34,7 @@ router.post('/delete', function (req, res) {
}) })
router.post('/create', function (req, res) { router.post('/create', function (req, res) {
Notes.create(userId, req.body.title, req.body.text, masterKey) Notes.create(req.io, userId, req.body.title, req.body.text, masterKey)
.then( id => res.send({id}) ) .then( id => res.send({id}) )
}) })

View File

@ -2,12 +2,15 @@ var express = require('express')
var router = express.Router() var router = express.Router()
let QuickNote = require('@models/QuickNote'); let QuickNote = require('@models/QuickNote');
let userId = null let userId = null
let masterKey = null
// middleware that is specific to this router // middleware that is specific to this router
router.use(function setUserId (req, res, next) { router.use(function setUserId (req, res, next) {
if(userId = req.headers.userId){ if(userId = req.headers.userId){
userId = req.headers.userId userId = req.headers.userId
masterKey = req.headers.masterKey
} }
next() next()
@ -15,19 +18,19 @@ router.use(function setUserId (req, res, next) {
//Get quick note text //Get quick note text
router.post('/get', function (req, res) { router.post('/get', function (req, res) {
QuickNote.get(userId) QuickNote.get(userId, masterKey)
.then( data => res.send(data) ) .then( data => res.send(data) )
}) })
//Push text to quick note //Push text to quick note
router.post('/update', function (req, res) { router.post('/update', function (req, res) {
QuickNote.update(userId, req.body.pushText) QuickNote.update(req.io, userId, req.body.pushText, masterKey)
.then( data => res.send(data) ) .then( data => res.send(data) )
}) })
//Change quick note to a new note //Push text to quick note
router.post('/change', function (req, res) { router.post('/new', function (req, res) {
QuickNote.change(userId, req.body.noteId) QuickNote.newNote(userId)
.then( data => res.send(data) ) .then( data => res.send(data) )
}) })