Created a uniform menu for notes that works on mobile
Added list sorting Added shared notes Fixed some little bugs here and there
This commit is contained in:
parent
2828cc9462
commit
de646cf1de
@ -5,6 +5,8 @@
|
||||
|
||||
<router-view />
|
||||
|
||||
<global-notification />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -15,6 +17,7 @@
|
||||
export default {
|
||||
components: {
|
||||
'global-site-menu': require('@/components/GlobalSiteMenu.vue').default,
|
||||
'global-notification':require('@/components/GlobalNotificationComponent.vue').default
|
||||
},
|
||||
data: function(){
|
||||
return {
|
||||
|
@ -21,6 +21,14 @@
|
||||
--text_color: #3d3d3d;
|
||||
--outline_color: rgba(34,36,38,.15);
|
||||
--border_color: rgba(34,36,38,.20);
|
||||
|
||||
/*Global purple menu styles */
|
||||
--menu-border: #534c68;
|
||||
--menu-background: #221f2b;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
div.ui.basic.segment.no-fluf-segment {
|
||||
@ -28,7 +36,7 @@ div.ui.basic.segment.no-fluf-segment {
|
||||
}
|
||||
|
||||
/* OVERWRITE DEFAULT SEMANTIC STYLES FOR CUSTOM/NIGHT MODES*/
|
||||
body{
|
||||
body {
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_color);
|
||||
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
@ -101,6 +109,77 @@ a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*//
|
||||
// Purple Global Menu
|
||||
//*/
|
||||
.note-menu {
|
||||
width: 100%;
|
||||
/*display: block;*/
|
||||
display: inline-table;
|
||||
background: var(--menu-background);
|
||||
color: white;
|
||||
/*overflow: hidden;*/
|
||||
border: 1px solid var(--menu-border);
|
||||
/*height: 50px;*/
|
||||
}
|
||||
.note-menu > .nm-button {
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
font-size: 1.2em;
|
||||
vertical-align: middle;
|
||||
/*height: 40px;*/
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
}
|
||||
.nm-button i.icon {
|
||||
margin: 0;
|
||||
}
|
||||
.nm-button span {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.nm-button.right {
|
||||
float: right;
|
||||
border-left: 1px solid var(--menu-border);
|
||||
}
|
||||
.nm-button:hover {
|
||||
background-color: #534c68;
|
||||
color: white;
|
||||
}
|
||||
.nm-button + .nm-button {
|
||||
border-left: 1px solid #534c68;
|
||||
}
|
||||
/*.shrink-icons-on-mobile.note-menu span {
|
||||
display: none;
|
||||
}*/
|
||||
|
||||
/* Shrink button text for mobile */
|
||||
@media only screen and (max-width: 740px) {
|
||||
.note-menu .nm-button span {
|
||||
font-size: 0.7em;
|
||||
line-height: 0.4em;
|
||||
margin-left: 0;
|
||||
}
|
||||
.nm-button i.icon {
|
||||
width: 100%;
|
||||
}
|
||||
/*prevents buttons from being jammed into corners of round phones*/
|
||||
.shrink-icons-on-mobile.note-menu {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.shrink-icons-on-mobile .nm-button {
|
||||
padding: 2px 3px;
|
||||
}
|
||||
.shrink-icons-on-mobile .nm-button i.icon {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
}
|
||||
|
||||
/*//
|
||||
// Purple Global Menu
|
||||
//*/
|
||||
|
||||
.note-status-indicator {
|
||||
position: absolute;
|
||||
width: 100px;
|
||||
@ -117,16 +196,17 @@ a:hover {
|
||||
/* squire text styles */
|
||||
.squire-box {
|
||||
border: none;
|
||||
height: calc(100% - 60px);
|
||||
height: calc(100% - 69px);
|
||||
box-sizing: border-box;
|
||||
padding: 10px 15px 40px;
|
||||
padding: 10px 15px 10px;
|
||||
background: transparent;
|
||||
overflow-x: scroll;
|
||||
/*color: var(--text_color);*/
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
word-wrap: break-word;
|
||||
border-bottom: 1px solid #ccc;
|
||||
/*border-bottom: 1px solid #ccc;*/
|
||||
scrollbar-width: none;
|
||||
}
|
||||
/*Makes the first line real big */
|
||||
.squire-box p:first-child {
|
||||
|
@ -1,27 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
|
||||
<div class="color-picker" :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
|
||||
|
||||
<div class="floating-buttons">
|
||||
<div class="ui basic segment">
|
||||
<div class="ui fluid buttons">
|
||||
<div class="ui compact button" v-on:click="closeThisBitch">
|
||||
<i class="close icon"></i>
|
||||
Close
|
||||
</div>
|
||||
<div class="ui compact button" v-on:click="clearStyles">
|
||||
<i class="refresh icon"></i>
|
||||
Clear All Styles
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :style="{ 'background-color':allStyles['noteBackground'], 'color':allStyles['noteText']}">
|
||||
<div class="ui basic segment">
|
||||
<div class="ui grid">
|
||||
|
||||
<div class="ui sixteen wide center aligned column">
|
||||
<div class="ui fluid button" v-on:click="clearStyles">
|
||||
<i class="refresh icon"></i>
|
||||
Clear All Styles
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="sixteen wide column">
|
||||
<br>
|
||||
@ -62,8 +52,7 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="shade-boy" v-on:click="closeThisBitch"></div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -157,43 +146,6 @@
|
||||
}
|
||||
</script>
|
||||
<style type="text/css" scoped>
|
||||
.shade-boy {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 250;
|
||||
cursor: pointer;
|
||||
}
|
||||
.floating-buttons {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 444;
|
||||
right: 20px;
|
||||
left: calc(30% + 10px)
|
||||
}
|
||||
.color-picker {
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_color);
|
||||
|
||||
position: fixed;
|
||||
|
||||
/*height: 100px;*/
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 30%;
|
||||
bottom: 0;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
z-index: 500;
|
||||
border-left: 1px solid;
|
||||
border-color: var(--border_color) !important;
|
||||
|
||||
overflow-x: scroll;
|
||||
|
||||
}
|
||||
.icon-button {
|
||||
height: 40px;
|
||||
width: 14.2%;
|
||||
|
@ -7,11 +7,9 @@
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<form>
|
||||
<form data-tooltip="Upload File" data-inverted>
|
||||
<label :for="`upfile-${noteId}`" class="clickable">
|
||||
<i class="upload icon"></i>
|
||||
<span v-if="uploadPercentage != 0">{{uploadPercentage}}%</span>
|
||||
<span v-else>Upload</span>
|
||||
<nm-button icon="upload" :text="uploadStatusText"/>
|
||||
</label>
|
||||
<input class="hidden-up" type="file" :id="`upfile-${noteId}`" ref="file" v-on:change="handleFileUpload()" />
|
||||
<!-- <button v-if="file" v-on:click="uploadFileToServer()">Submit</button> -->
|
||||
@ -23,10 +21,13 @@
|
||||
export default {
|
||||
name: 'FileUploadButton',
|
||||
props: [ 'noteId' ],
|
||||
components: {
|
||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
file: null,
|
||||
uploadPercentage: 0,
|
||||
uploadStatusText: 'Upload',
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
@ -44,12 +45,12 @@
|
||||
formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
onUploadProgress: ( progressEvent ) => {
|
||||
this.uploadPercentage = parseInt(
|
||||
this.uploadStatusText = parseInt(
|
||||
Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) )
|
||||
}
|
||||
}
|
||||
).then(results => {
|
||||
this.uploadPercentage = 'Working'
|
||||
this.uploadStatusText = 'Working'
|
||||
this.file = null
|
||||
|
||||
// console.log('File upload results')
|
||||
@ -58,17 +59,19 @@
|
||||
const name = results.data.fileName
|
||||
const location = results.data.goodFileName
|
||||
|
||||
this.$bus.$emit('notification', 'Processing Upload')
|
||||
|
||||
if(name && location){
|
||||
|
||||
this.uploadPercentage = 0
|
||||
this.uploadStatusText = 'Upload File'
|
||||
|
||||
const imageCode = `<img alt="image" src="/api/static/${location}">`
|
||||
const imageCode = `<img alt="image" src="/api/static/thumb_${location}">`
|
||||
|
||||
this.$bus.$emit('new_file_upload', {noteId: this.noteId, imageCode})
|
||||
}
|
||||
})
|
||||
.catch(results => {
|
||||
this.uploadPercentage = 0
|
||||
this.uploadStatusText = 0
|
||||
})
|
||||
},
|
||||
handleFileUpload() {
|
||||
|
82
client/src/components/GlobalNotificationComponent.vue
Normal file
82
client/src/components/GlobalNotificationComponent.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.popup-body {
|
||||
position: fixed;
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
min-height: 50px;
|
||||
min-width: 200px;
|
||||
max-width: calc(100% - 20px);
|
||||
z-index: 1002;
|
||||
border-top: 2px solid #21ba45;
|
||||
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
|
||||
border-top-right-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
|
||||
color: var(--text_color);
|
||||
background-color: var(--background_color);
|
||||
}
|
||||
.popup-row {
|
||||
padding: 1em 5px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.popup-row > span {
|
||||
width: calc(100% - 50px);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px 0;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
.popup-row + .popup-row {
|
||||
border-top: 1px solid #000;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="popup-body" v-on:click="dismiss" v-if="notifications.length > 0">
|
||||
<div class="popup-row" v-for="item in notifications">
|
||||
<i class="disabled angle left icon"></i>
|
||||
<span>{{ item }}</span>
|
||||
<i class="disabled angle right icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'GlobalNotificationComponent',
|
||||
data () {
|
||||
return {
|
||||
notifications: [],
|
||||
totalTimeout: null,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
this.$bus.$on('notification', info => {
|
||||
this.displayNotification(info)
|
||||
})
|
||||
},
|
||||
mounted(){
|
||||
|
||||
|
||||
},
|
||||
methods: {
|
||||
displayNotification(newNotification){
|
||||
this.notifications.push(newNotification)
|
||||
clearTimeout(this.totalTimeout)
|
||||
this.totalTimeout = setTimeout(() => {
|
||||
this.dismiss()
|
||||
}, 4000)
|
||||
},
|
||||
dismiss(){
|
||||
this.notifications = []
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -163,6 +163,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<div v-on:click="updateFastFilters(3)" class="menu-item menu-button">
|
||||
<i class="mail icon"></i>Inbox
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-section" v-if="loggedIn">
|
||||
<div v-on:click="updateFastFilters(2)" class="menu-item menu-button">
|
||||
<i class="archive icon"></i>Archived
|
||||
@ -239,6 +245,7 @@
|
||||
if(this.mobile){
|
||||
this.menuOpen = false
|
||||
}
|
||||
|
||||
},
|
||||
computed: {
|
||||
loggedIn () {
|
||||
@ -279,6 +286,7 @@
|
||||
})
|
||||
},
|
||||
destroyLoginToken() {
|
||||
this.$bus.$emit('notification', 'Logged Out')
|
||||
this.$store.commit('destroyLoginToken')
|
||||
this.$router.push('/')
|
||||
},
|
||||
@ -309,6 +317,7 @@
|
||||
'withLinks', // 'Only Show Notes with Links'
|
||||
'withTags', // 'Only Show Notes with Tags'
|
||||
'onlyArchived', //'Only Show Archived Notes'
|
||||
'onlyShowSharedNotes', //Only show shared notes
|
||||
]
|
||||
|
||||
let filter = {}
|
||||
|
@ -17,30 +17,23 @@
|
||||
|
||||
<div class="note-menu">
|
||||
|
||||
<div class="nm-button" v-on:click="close">
|
||||
<i class="close icon"></i>
|
||||
</div>
|
||||
<div class="nm-button" v-on:click="toggleList('ol')">
|
||||
<i class="list ol icon"></i>
|
||||
</div>
|
||||
<div class="nm-button" v-on:click="toggleList('ul')">
|
||||
<i class="tasks icon"></i>
|
||||
</div>
|
||||
<div class="nm-button" v-on:click="toggleBold()">
|
||||
<i class="bold icon"></i>
|
||||
</div>
|
||||
<nm-button v-on:click.native="close" icon="close" />
|
||||
|
||||
<div class="nm-button" v-on:click="toggleItalic()">
|
||||
<i class="quote left icon"></i>
|
||||
</div>
|
||||
<nm-button v-on:click.native="toggleList('ol')" icon="list ol" />
|
||||
|
||||
<nm-button v-on:click.native="toggleList('ul')" icon="tasks" />
|
||||
|
||||
<nm-button v-on:click.native="toggleBold()" icon="bold" />
|
||||
|
||||
<nm-button v-on:click.native="toggleItalic()" icon="quote left" />
|
||||
|
||||
<div class="nm-button" v-on:click="modifyFont('2.286em') ">
|
||||
<i class="text height icon"></i>
|
||||
</div>
|
||||
<nm-button v-on:click.native="modifyFont('1.4em')" icon="text height" />
|
||||
|
||||
<div v-if="usersOnNote > 1" class="nm-button">
|
||||
<i class="green user circle icon"></i>{{usersOnNote}}
|
||||
</div>
|
||||
<nm-button v-on:click.native="undoCustom()" icon="undo" />
|
||||
|
||||
<nm-button v-if="usersOnNote > 1" icon="green user circle" />
|
||||
|
||||
<nm-button icon="ellipsis horizontal" v-on:click.native="showNoteOptions = !showNoteOptions" />
|
||||
|
||||
</div>
|
||||
|
||||
@ -51,47 +44,136 @@
|
||||
<div class="ui green button">{{statusText}}</div>
|
||||
</span>
|
||||
|
||||
<color-picker
|
||||
v-if="colorPickerVisible"
|
||||
:location="colorPickerLocation"
|
||||
@changeColor="onChangeColor"
|
||||
@close="onCloseColorChanger"
|
||||
:style-object="styleObject"
|
||||
/>
|
||||
|
||||
<!-- Note options on the bottom of note -->
|
||||
<div class="all-settings" :class="{ 'low-settings':!extraToolbarsVisible }">
|
||||
|
||||
<div class="note-menu">
|
||||
|
||||
<!-- <note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/><br> -->
|
||||
<div class="note-menu shrink-icons-on-mobile">
|
||||
|
||||
<!-- Pin Button -->
|
||||
<div @click="onToggleArchived" class="nm-button">
|
||||
<i class="archive icon" :class="{green:(archived == 1)}"></i>
|
||||
{{(archived == 1)?'Archived':'Archive'}}
|
||||
</div>
|
||||
<!-- archive button -->
|
||||
<div @click="onTogglePinned" class="nm-button">
|
||||
<i class="pin icon" :class="{green:(pinned == 1)}"></i>
|
||||
{{(pinned == 1)?'Pinned':'Pin'}}
|
||||
</div>
|
||||
<!-- colors button -->
|
||||
<span class="nm-button" v-on:click="showColorPicker">
|
||||
<i class="paint brush icon"></i>
|
||||
Colors
|
||||
</span>
|
||||
<nm-button
|
||||
v-on:click.native="onToggleArchived"
|
||||
:icon="(archived == 1)?'green archive':'archive'"
|
||||
:text="(archived == 1)?'Archived':'Archive'"
|
||||
tip="Show in archive"
|
||||
:showText="true"
|
||||
></nm-button>
|
||||
|
||||
<!-- archive button -->
|
||||
<nm-button
|
||||
v-on:click.native="onTogglePinned"
|
||||
:icon="(pinned == 1)?'green pin':'pin'"
|
||||
:text="(pinned == 1)?'Pinned':'Pin'"
|
||||
tip="Pin to top of list"
|
||||
:showText="true"
|
||||
></nm-button>
|
||||
|
||||
<!-- colors button -->
|
||||
<nm-button
|
||||
v-on:click.native="showColorPicker"
|
||||
icon="paint brush"
|
||||
text="Colors"
|
||||
tip="Colors"
|
||||
></nm-button>
|
||||
|
||||
<!-- add images panel -->
|
||||
<nm-button
|
||||
v-on:click.native="showFilesSideMenu = !showFilesSideMenu"
|
||||
icon="image"
|
||||
text="Images"
|
||||
tip="Images"
|
||||
></nm-button>
|
||||
|
||||
<!-- Tags -->
|
||||
<nm-button
|
||||
v-on:click.native="showTagSlideMenu = !showTagSlideMenu"
|
||||
icon="tags"
|
||||
text="Tags"
|
||||
tip="Tags"
|
||||
></nm-button>
|
||||
|
||||
<!-- attachment button -->
|
||||
<div class="nm-button" v-on:click="openEditAttachment">
|
||||
<i class="folder icon"></i> Files
|
||||
</div>
|
||||
<!-- file upload button -->
|
||||
<file-upload-button class="nm-button" :noteId="noteid" />
|
||||
<file-upload-button
|
||||
class="nm-button"
|
||||
:noteId="noteid" />
|
||||
|
||||
<!-- files button -->
|
||||
<nm-button
|
||||
v-on:click.native="openEditAttachment"
|
||||
icon="folder"
|
||||
text="Files"
|
||||
tip="Files on Note"
|
||||
:showText="true"
|
||||
></nm-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="shade" v-on:click="showAllSettings = false"></div> -->
|
||||
|
||||
<!-- Side slide menus for colors, tags and images -->
|
||||
|
||||
<side-slide-menu v-if="colorPickerVisible" v-on:close="colorPickerVisible = false" name="colors">
|
||||
<color-picker
|
||||
@changeColor="onChangeColor"
|
||||
@close="onCloseColorChanger"
|
||||
:style-object="styleObject"
|
||||
/>
|
||||
</side-slide-menu>
|
||||
|
||||
<side-slide-menu v-if="showTagSlideMenu" v-on:close="showTagSlideMenu = false" name="tags">
|
||||
<div class="ui basic segment">
|
||||
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/>
|
||||
</div>
|
||||
</side-slide-menu>
|
||||
|
||||
<side-slide-menu v-if="showFilesSideMenu" v-on:close="showFilesSideMenu = false" name="images">
|
||||
<div class="ui basic segment">
|
||||
<simple-attachment-note
|
||||
v-on:close="showFilesSideMenu = false"
|
||||
:note-id="noteid"
|
||||
:squire-editor="editor">
|
||||
</simple-attachment-note>
|
||||
</div>
|
||||
</side-slide-menu>
|
||||
|
||||
<side-slide-menu v-if="showNoteOptions" v-on:close="showNoteOptions = false" name="note-options">
|
||||
<div class="ui basic padded segment">
|
||||
<div class="ui grid">
|
||||
<div class="sixteen wide column">
|
||||
<h2>Additional Note Options</h2>
|
||||
</div>
|
||||
<div class="sixteen wide column">
|
||||
<div class="ui labeled icon fluid basic button" v-on:click="sortList">
|
||||
<i class="sort amount up icon"></i>
|
||||
Sort List items (Move checked to bottom)
|
||||
</div>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<div class="ui labeled icon fluid basic button" v-on:click="deleteCompletedListItems">
|
||||
<i class="trash icon"></i>
|
||||
Delete Checked Items
|
||||
</div>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<div class="ui labeled icon fluid basic button" v-on:click="uncheckAllListItems">
|
||||
<i class="list ul icon"></i>
|
||||
Uncheck all Checked items
|
||||
</div>
|
||||
</div>
|
||||
<div class="sixteen wide column">
|
||||
<div class="ui labeled icon fluid basic button" v-on:click="undoCustom">
|
||||
<i class="undo icon"></i>
|
||||
Undo last change
|
||||
</div>
|
||||
</div>
|
||||
<div class="sixteen wide column" v-if="rawTextId > 0">
|
||||
<share-note-component
|
||||
:note-id="noteid"
|
||||
:raw-text-id="rawTextId"
|
||||
:share-username="shareUsername"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</side-slide-menu>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@ -105,10 +187,15 @@
|
||||
name: 'InputNotes',
|
||||
props: [ 'noteid', 'position' ],
|
||||
components:{
|
||||
'note-tag-edit': require('@/components/NoteTagEdit.vue').default,
|
||||
'color-picker': require('@/components/ColorPicker.vue').default,
|
||||
'file-upload-button': require('@/components/FileUploadButton.vue').default,
|
||||
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
|
||||
'note-tag-edit': () => import('@/components/NoteTagEdit.vue'),
|
||||
'color-picker': () => import('@/components/ColorPicker.vue'),
|
||||
'file-upload-button': () => import('@/components/FileUploadButton.vue'),
|
||||
// 'delete-button': () => import('@/components/NoteDeleteButtonComponent.vue'),
|
||||
'side-slide-menu': () => import('@/components/SideSlideMenuComponent.vue'),
|
||||
'simple-attachment-note': () => import('@/components/SimpleAttachmentNoteComponent.vue'),
|
||||
'share-note-component': () => import('@/components/ShareNoteComponent.vue'),
|
||||
|
||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
@ -116,6 +203,8 @@
|
||||
loadingMessage: 'Loading Note',
|
||||
currentNoteId: 0,
|
||||
noteText: '',
|
||||
rawTextId: 0,
|
||||
shareUsername: null,
|
||||
diffNoteText: '',
|
||||
statusText: 'Saved',
|
||||
lastNoteHash: null,
|
||||
@ -130,7 +219,7 @@
|
||||
styleObject: { 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, //Style object. Determines colors and badges
|
||||
|
||||
sizeDown: false, //Used to animate close state
|
||||
colorPickerVisible: false,
|
||||
|
||||
colorPickerLocation: null,
|
||||
|
||||
tinymce: null, //Initialized editor instance
|
||||
@ -145,6 +234,10 @@
|
||||
usersOnNote: 0,
|
||||
|
||||
extraToolbarsVisible: true,
|
||||
showTagSlideMenu: false,
|
||||
colorPickerVisible: false,
|
||||
showFilesSideMenu: false,
|
||||
showNoteOptions: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -168,12 +261,13 @@
|
||||
if(this.noteid == noteId && this.editor){
|
||||
this.editor.moveCursorToEnd()
|
||||
this.editor.insertHTML(imageCode)
|
||||
this.save()
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeDestroy(){
|
||||
|
||||
this.$io.emit('leave_room', this.noteid)
|
||||
this.$io.emit('leave_room', this.rawTextId)
|
||||
|
||||
document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
|
||||
|
||||
@ -189,24 +283,7 @@
|
||||
this.$nextTick(() => {
|
||||
|
||||
this.loadNote(this.noteid)
|
||||
|
||||
//Tell server to push this note into a room
|
||||
this.$io.emit('join_room', this.noteid)
|
||||
|
||||
this.$io.on('update_user_count', userCount => {
|
||||
this.usersOnNote = userCount
|
||||
})
|
||||
|
||||
//Server will hand deliver diffs from other notes to this one
|
||||
this.$io.on('incoming_diff', incomingDiffData => {
|
||||
this.patchText(incomingDiffData)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
},
|
||||
methods: {
|
||||
initSquire(){
|
||||
@ -215,7 +292,7 @@
|
||||
this.editor = new Squire( this.$refs.squirebox, {blockTag: 'p' })
|
||||
this.setText(this.noteText)
|
||||
|
||||
//Open links when clicked in editor
|
||||
//Click Event - Open links when clicked in editor or toggle checks
|
||||
this.editor.addEventListener('click', e => {
|
||||
|
||||
//Link clicked in editor - open link
|
||||
@ -284,6 +361,7 @@
|
||||
},
|
||||
//If nothing is selected, select the entire line
|
||||
selectLineIfNoSelect(){
|
||||
|
||||
//Select entire line if range is not set
|
||||
let selection = this.editor.getSelection()
|
||||
|
||||
@ -297,6 +375,7 @@
|
||||
}
|
||||
},
|
||||
modifyFont(inSize){
|
||||
|
||||
this.selectLineIfNoSelect()
|
||||
|
||||
let fontInfo = this.editor.getFontInfo()
|
||||
@ -338,6 +417,149 @@
|
||||
this.editor.italic()
|
||||
}
|
||||
},
|
||||
undoCustom(){
|
||||
//The same as pressing CTRL + Z
|
||||
// this.editor.focus()
|
||||
// document.execCommand("undo", false, null)
|
||||
this.editor.undo()
|
||||
},
|
||||
uncheckAllListItems(){
|
||||
//
|
||||
// Uncheck All List Items
|
||||
//
|
||||
|
||||
//Close menu if user is on mobile, then sort list
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
this.showNoteOptions = false
|
||||
}
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
Array.from( container.getElementsByClassName('active') ).forEach(item => {
|
||||
item.classList.remove('active');
|
||||
})
|
||||
},
|
||||
deleteCompletedListItems(){
|
||||
//
|
||||
// Delete Completed List Items
|
||||
//
|
||||
|
||||
//Close menu if user is on mobile, then sort list
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
this.showNoteOptions = false
|
||||
}
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
//Go through each item, on first level, look for Unordered Lists
|
||||
container.childNodes.forEach( (node) => {
|
||||
if(node.nodeName == 'UL'){
|
||||
|
||||
//Create two categories, done and not done list items
|
||||
let undoneElements = document.createDocumentFragment()
|
||||
|
||||
//Go through each item in each list we found
|
||||
node.childNodes.forEach( (checkListItem, index) => {
|
||||
|
||||
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
|
||||
if(checkListItem.nodeName == 'UL'){
|
||||
return
|
||||
}
|
||||
|
||||
//Check if list item has active class
|
||||
const checkedItem = checkListItem.classList.contains('active')
|
||||
|
||||
//Check if the next item is a list, Keep lists with intented items together
|
||||
let sublist = null
|
||||
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
|
||||
sublist = node.childNodes[index+1]
|
||||
}
|
||||
|
||||
//Push checked items and their sub lists to the done set
|
||||
if(!checkedItem){
|
||||
|
||||
undoneElements.appendChild( checkListItem.cloneNode(true) )
|
||||
if(sublist){
|
||||
undoneElements.appendChild( sublist.cloneNode(true) )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
//Remove all HTML from node, push unfinished items, then finished below them
|
||||
node.innerHTML = null
|
||||
node.appendChild(undoneElements)
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
sortList(){
|
||||
//
|
||||
// Sort list, checked at the bottom, unchecked at the top
|
||||
//
|
||||
|
||||
//Close menu if user is on mobile, then sort list
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
this.showNoteOptions = false
|
||||
}
|
||||
|
||||
//Fetch the container
|
||||
let container = document.getElementById('squire-id')
|
||||
|
||||
//Go through each item, on first level, look for Unordered Lists
|
||||
container.childNodes.forEach( (node) => {
|
||||
if(node.nodeName == 'UL'){
|
||||
|
||||
//Create two categories, done and not done list items
|
||||
let doneElements = document.createDocumentFragment()
|
||||
let undoneElements = document.createDocumentFragment()
|
||||
|
||||
//Go through each item in each list we found
|
||||
node.childNodes.forEach( (checkListItem, index) => {
|
||||
|
||||
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
|
||||
if(checkListItem.nodeName == 'UL'){
|
||||
return
|
||||
}
|
||||
|
||||
//Check if list item has active class
|
||||
const checkedItem = checkListItem.classList.contains('active')
|
||||
|
||||
//Check if the next item is a list, Keep lists with intented items together
|
||||
let sublist = null
|
||||
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
|
||||
sublist = node.childNodes[index+1]
|
||||
}
|
||||
|
||||
//Push checked items and their sub lists to the done set
|
||||
if(checkedItem){
|
||||
|
||||
doneElements.appendChild( checkListItem.cloneNode(true) )
|
||||
if(sublist){
|
||||
doneElements.appendChild( sublist.cloneNode(true) )
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
undoneElements.appendChild( checkListItem.cloneNode(true) )
|
||||
if(sublist){
|
||||
undoneElements.appendChild( sublist.cloneNode(true) )
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
//Remove all HTML from node, push unfinished items, then finished below them
|
||||
node.innerHTML = null
|
||||
node.appendChild(undoneElements)
|
||||
node.appendChild(doneElements)
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
setText(inText){
|
||||
|
||||
this.editor.setHTML(inText)
|
||||
@ -349,13 +571,16 @@
|
||||
return this.editor.getHTML()
|
||||
},
|
||||
showColorPicker(event){
|
||||
|
||||
this.colorPickerVisible = !this.colorPickerVisible
|
||||
this.colorPickerLocation = {'x':event.clientX, 'y':event.clientY}
|
||||
},
|
||||
openEditAttachment(){
|
||||
|
||||
this.$router.push('/attachments/note/'+this.currentNoteId)
|
||||
},
|
||||
onTogglePinned(){
|
||||
|
||||
if(this.pinned == 0){
|
||||
this.pinned = 1
|
||||
} else {
|
||||
@ -366,6 +591,7 @@
|
||||
this.save()
|
||||
},
|
||||
onToggleArchived(){
|
||||
|
||||
if(this.archived == 0){
|
||||
this.archived = 1
|
||||
} else {
|
||||
@ -376,6 +602,7 @@
|
||||
this.save()
|
||||
},
|
||||
onCloseColorChanger(){
|
||||
|
||||
this.colorPickerVisible = false
|
||||
},
|
||||
onChangeColor(newStyleObject){
|
||||
@ -403,6 +630,8 @@
|
||||
|
||||
//Set up local data
|
||||
vm.currentNoteId = noteId
|
||||
this.rawTextId = response.data.rawTextId
|
||||
this.shareUsername = response.data.shareUsername
|
||||
|
||||
vm.noteText = response.data.text
|
||||
vm.diffNoteText = response.data.text
|
||||
@ -423,7 +652,8 @@
|
||||
this.loading = false
|
||||
|
||||
vm.$nextTick(() => {
|
||||
// this.initTinyMce()
|
||||
|
||||
this.setupWebSockets()
|
||||
this.initSquire()
|
||||
})
|
||||
|
||||
@ -473,8 +703,8 @@
|
||||
|
||||
// console.log(patch_text)
|
||||
this.$io.emit('note_diff', {
|
||||
id:this.currentNoteId,
|
||||
diff:patch_text
|
||||
id: this.rawTextId,
|
||||
diff: patch_text
|
||||
})
|
||||
|
||||
|
||||
@ -575,9 +805,6 @@
|
||||
|
||||
}, 20)
|
||||
// }
|
||||
|
||||
|
||||
|
||||
},
|
||||
xpath(el) {
|
||||
//Skip things we can't use
|
||||
@ -592,12 +819,13 @@
|
||||
const sames = [].filter.call(el.parentNode.children, function (x) { return x.tagName == el.tagName })
|
||||
return this.xpath(el.parentNode) + '/' + el.tagName.toLowerCase() + (sames.length > 1 ? '['+([].indexOf.call(sames, el)+1)+']' : '')
|
||||
},
|
||||
getElementByXPath(xpath) {
|
||||
getElementByXPath(xpath){
|
||||
|
||||
return new XPathEvaluator()
|
||||
.createExpression(xpath)
|
||||
.evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE) .singleNodeValue
|
||||
},
|
||||
onKeyup(event){
|
||||
onKeyup(event){
|
||||
|
||||
this.statusText = 'Save'
|
||||
|
||||
@ -661,7 +889,6 @@
|
||||
})
|
||||
// }, 300)
|
||||
})
|
||||
|
||||
},
|
||||
checkForUpdatedNote(){
|
||||
|
||||
@ -669,7 +896,7 @@
|
||||
|
||||
//If user leaves page then returns to page, reload the first batch
|
||||
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
|
||||
console.log('Checking for note updates after visibility change.')
|
||||
// console.log('Checking for note updates after visibility change.')
|
||||
|
||||
const postData = {
|
||||
noteId:this.currentNoteId,
|
||||
@ -726,7 +953,19 @@
|
||||
return
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
setupWebSockets(){
|
||||
//Tell server to push this note into a room
|
||||
this.$io.emit('join_room', this.rawTextId)
|
||||
|
||||
this.$io.on('update_user_count', userCount => {
|
||||
this.usersOnNote = userCount
|
||||
})
|
||||
|
||||
//Server will hand deliver diffs from other notes to this one
|
||||
this.$io.on('incoming_diff', incomingDiffData => {
|
||||
this.patchText(incomingDiffData)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -734,38 +973,6 @@
|
||||
|
||||
<style type="text/css" scoped>
|
||||
|
||||
/* squire note menu button */
|
||||
|
||||
.note-menu {
|
||||
width: 100%;
|
||||
display: block;
|
||||
background: #221f2b;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
border: 1px solid #534c68;
|
||||
}
|
||||
.note-menu > .nm-button {
|
||||
padding: 10px 10px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
font-size: 1.1em;
|
||||
vertical-align: middle;
|
||||
/*height: 40px;*/
|
||||
display: table-cell;
|
||||
}
|
||||
.nm-button > i {
|
||||
font-size: 1.1em;
|
||||
margin: 0;
|
||||
}
|
||||
.nm-button:hover {
|
||||
background-color: #534c68;
|
||||
color: white;
|
||||
}
|
||||
.nm-button + .nm-button {
|
||||
border-left: 1px solid #534c68;
|
||||
}
|
||||
|
||||
/* squire styles */
|
||||
|
||||
/*Settings manager styles */
|
||||
|
44
client/src/components/NoteMenuButtonComponent.vue
Normal file
44
client/src/components/NoteMenuButtonComponent.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="nm-button" :class="moreClass" :data-tooltip="tip" data-inverted>
|
||||
<!-- Display Icon and text -->
|
||||
<i v-if="icon" :class="`${icon} icon`"></i>
|
||||
<span v-if="(text && mobile) || (text && showText)">{{text}}</span>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/*
|
||||
Menu button
|
||||
Single Icon View
|
||||
Single Icon With small text on mobile
|
||||
Tooltips on desktop
|
||||
Tooltip above or below
|
||||
|
||||
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'NoteMenuButtonComponent',
|
||||
props: [ 'icon', 'text', 'tooltip', 'moreClass', 'showText', 'tip'],
|
||||
data () {
|
||||
return {
|
||||
files: [],
|
||||
mobile: false,
|
||||
showTooltip: false,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
this.mobile = this.$store.getters.getIsUserOnMobile
|
||||
},
|
||||
mounted(){
|
||||
// console.log('Im a button')
|
||||
},
|
||||
methods: {
|
||||
onFileClick(file){
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,41 +1,36 @@
|
||||
<template>
|
||||
<div v-on:click="fullTagEdit = true">
|
||||
|
||||
<!-- simple string view -->
|
||||
<!-- class="ui basic segment" -->
|
||||
<div v-if="!fullTagEdit">
|
||||
<div class="simple-tag-display">
|
||||
|
||||
<!-- Show Loading -->
|
||||
<span v-if="!loaded">Loading Tags...</span>
|
||||
|
||||
<!-- Default count -->
|
||||
<span v-if="loaded">
|
||||
<i class="tags icon"></i> <b>{{tags.length}} Tag<span v-if="tags.length > 1">s</span></b>
|
||||
</span>
|
||||
|
||||
<!-- No tags default text -->
|
||||
<span v-if="tags.length == 0 && loaded" class="ui small compact basic green button">
|
||||
<i class="plus icon"></i> Add a tag
|
||||
</span>
|
||||
|
||||
<!-- display tags in comma delimited list -->
|
||||
<span v-if="tags.length > 0">
|
||||
<span v-for="(tag, i) in tags"><span v-if="i > 0">, </span>{{ucWords(tag.text)}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<!-- hover over view -->
|
||||
<div v-if="fullTagEdit" class="full-tag-area fade-in-fwd" v-on:mouseleave="fullTagEdit = false; clearSuggestions()">
|
||||
<div v-if="fullTagEdit" class="full-tag-area fade-in-fwd">
|
||||
|
||||
<!-- existing tags -->
|
||||
<div class="delete-tag-display" v-if="tags.length > 0">
|
||||
<div class="ui icon large label" v-for="tag in tags" :class="{ 'green':(newTagInput == tag.text) }">
|
||||
{{ucWords(tag.text)}} <i class="delete icon" v-on:click="removeTag(tag.id)"></i>
|
||||
<div class="ui grid">
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<h2><i class="green tags icon"></i>Edit Tags</h2>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<h3>All Tags</h3>
|
||||
<div v-if="allTags.length > 0">
|
||||
<div class="ui icon large label" v-for="tag in allTags" :class="{ 'green':isTagOnNote(tag.id) }">
|
||||
{{ ucWords(tag.text) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sixteen wide column">
|
||||
<h3>Tags on Note</h3>
|
||||
<div v-if="allTags.length > 0 && noteTagIds.length > 0">
|
||||
<div class="ui icon large label" v-for="tag in noteTagIds">
|
||||
{{ getTagTextById(tag['tagId']) }} <i class="delete icon" v-on:click="removeTag(tag['entryId'])"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- tag input and suggestion popup -->
|
||||
<div class="ui form">
|
||||
<input
|
||||
@ -70,9 +65,11 @@
|
||||
newTagInput: '',
|
||||
typeDebounce: null,
|
||||
|
||||
allTags: [],
|
||||
noteTagIds: [],
|
||||
suggestions: [],
|
||||
selection: 0,
|
||||
fullTagEdit: false,
|
||||
fullTagEdit: true,
|
||||
loaded: false,
|
||||
}
|
||||
},
|
||||
@ -85,14 +82,37 @@
|
||||
methods: {
|
||||
getTags(){
|
||||
//Get Note Tags -> /api/tags/get
|
||||
let vm = this
|
||||
axios.post('/api/tag/get', {'noteId': this.noteId})
|
||||
.then(response => {
|
||||
vm.loaded = true
|
||||
//Set up local data
|
||||
vm.tags = response.data
|
||||
.then( ({data}) => {
|
||||
|
||||
this.loaded = true
|
||||
|
||||
this.allTags = data.allTags
|
||||
this.noteTagIds = data.noteTagIds
|
||||
})
|
||||
},
|
||||
isTagOnNote(id){
|
||||
for (let i = 0; i < this.noteTagIds.length; i++) {
|
||||
const current = this.noteTagIds[i]
|
||||
if(current && current['tagId'] == id){
|
||||
return true
|
||||
}
|
||||
}
|
||||
},
|
||||
getTagTextById(id){
|
||||
let tag = this.getTagById(id)
|
||||
if(tag && tag.text){
|
||||
return this.ucWords( tag.text )
|
||||
}
|
||||
},
|
||||
getTagById(id){
|
||||
for (let i = 0; i < this.allTags.length; i++) {
|
||||
const current = this.allTags[i]
|
||||
if(current && current['id'] == id){
|
||||
return current
|
||||
}
|
||||
}
|
||||
},
|
||||
tagInput(event){
|
||||
let vm = this
|
||||
|
||||
@ -177,6 +197,7 @@
|
||||
})
|
||||
},
|
||||
onFocus(){
|
||||
return
|
||||
//Show suggested tags
|
||||
let vm = this
|
||||
let postData = {
|
||||
@ -211,10 +232,10 @@
|
||||
'tagId':tagId,
|
||||
'noteId':this.noteId
|
||||
}
|
||||
let vm = this
|
||||
|
||||
axios.post('/api/tag/removefromnote', postData)
|
||||
.then(response => {
|
||||
vm.getTags()
|
||||
this.getTags()
|
||||
})
|
||||
},
|
||||
clearSuggestions(){
|
||||
|
@ -40,36 +40,39 @@
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Toolbar on the bottom -->
|
||||
<div class="bottom aligned row icon-bar" @click.self="onClick(note.id)">
|
||||
<div class="six wide column clickable" @click="onClick(note.id)">
|
||||
{{$helpers.timeAgo(note.updated)}}
|
||||
</div>
|
||||
<div class="bottom aligned row" @click.self="onClick(note.id)">
|
||||
<div class="sixteen wide column">
|
||||
<div class="ui grid reduced-padding">
|
||||
|
||||
<div class="ten wide right aligned column">
|
||||
|
||||
<!-- ALways show delete button on mobile -->
|
||||
<delete-button :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }" :note-id="note.id" />
|
||||
<div class="thirteen wide column clickable icon-bar" @click="onClick(note.id)">
|
||||
<!-- {{$helpers.timeAgo(note.updated)}} -->
|
||||
<span v-if="note.tags">
|
||||
<span v-for="tag in (note.tags.split(','))" class="little-tag">{{ tag }}</span>
|
||||
</span>
|
||||
<span v-if="note.pinned == 1" data-position="top right" data-tooltip="Pinned" data-inverted="">
|
||||
<i class="green pin icon"></i>
|
||||
</span>
|
||||
<span v-if="note.archived == 1" data-position="top right" data-tooltip="Archived" data-inverted="">
|
||||
<i class="green archive icon"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="three wide right aligned column">
|
||||
<delete-button :class="{ 'hover-hide':(!$store.getters.getIsUserOnMobile) }" :note-id="note.id" />
|
||||
</div>
|
||||
|
||||
<span v-if="note.pinned == 1" data-position="top right" data-tooltip="Pinned" data-inverted="">
|
||||
<i class="green pin icon"></i>
|
||||
</span>
|
||||
<span v-if="note.archived == 1" data-position="top right" data-tooltip="Archived" data-inverted="">
|
||||
<i class="green archive icon"></i>
|
||||
</span>
|
||||
<span v-if="note.attachment_count > 0" v-on:click.stop="openEditAttachment" class="clickable">
|
||||
<i class="linkify icon"></i> {{note.attachment_count}}
|
||||
</span>
|
||||
<span v-if="note.tag_count == 1" data-position="top right" data-tooltip="Note has 1 tag" data-inverted="">
|
||||
<i class="tags icon"></i> {{note.tag_count}}
|
||||
</span>
|
||||
<span v-if="note.tag_count > 1" :data-tooltip="`Note has ${note.tag_count} tags`" data-position="top right" data-inverted="">
|
||||
<i class="tags icon"></i> {{note.tag_count}}
|
||||
</span>
|
||||
<div class="row" v-if="note.thumbs">
|
||||
<div class="tiny-thumb-box" v-on:click="openEditAttachment">
|
||||
<img v-for="thumb in note.thumbs.split(',').reverse()" class="tiny-thumb" :src="`/api/static/thumb_${thumb}`">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -145,13 +148,16 @@
|
||||
|
||||
/*Strict font sizes for card display*/
|
||||
.small-text, .small-text > p, .small-text > h1, .small-text > h2 {
|
||||
font-size: 1.0em !important;
|
||||
/*font-size: 1.0em !important;*/
|
||||
font-size: 15px !important;
|
||||
}
|
||||
.small-text > p, , .small-text > h1, .small-text > h2 {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.big-text, .big-text > p, .big-text > h1, .big-text > h2 {
|
||||
font-size: 1.3em !important;
|
||||
/*font-size: 1.3em !important;*/
|
||||
font-size: 16px !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
.big-text > p, .big-text > h1, .big-text > h2 {
|
||||
margin-bottom: 0.3em;
|
||||
@ -186,7 +192,7 @@
|
||||
/*box-shadow: 0 1px 2px 0 rgba(34,36,38,.15);*/
|
||||
box-shadow: 0 0px 5px 1px rgba(34,36,38,0);
|
||||
margin: 5px;
|
||||
padding: 1em 1.5em;
|
||||
padding: 0.7em 1em;
|
||||
border-radius: .28571429rem;
|
||||
border: 1px solid;
|
||||
border-color: var(--border_color);
|
||||
@ -205,11 +211,42 @@
|
||||
}
|
||||
.icon-bar {
|
||||
opacity: 0.8;
|
||||
margin-top: -2.2rem;
|
||||
/*margin-top: -2.2rem;*/
|
||||
}
|
||||
.hover-hide {
|
||||
opacity: 0.0;
|
||||
}
|
||||
.little-tag {
|
||||
font-size: 0.7em;
|
||||
padding: 5px 5px;
|
||||
border: 1px solid var(--border_color);
|
||||
margin: 5px 3px 0 0;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
line-height: 0.8em;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.tiny-thumb-box {
|
||||
max-height: 70px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
background-color: rgba(200, 200, 200, 0.2);
|
||||
white-space: nowrap;
|
||||
overflow-x: scroll;
|
||||
border: 1px solid var(--border_color);
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
text-align: center;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.tiny-thumb {
|
||||
max-height: 70px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.note-title-display-card:hover .icon-bar {
|
||||
opacity: 1;
|
||||
@ -262,7 +299,6 @@
|
||||
.note-title-display-card {
|
||||
width: calc(100% + 10px);
|
||||
margin: 0px -5px 10px -5px;
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -2,7 +2,7 @@
|
||||
<div class="ui form">
|
||||
<div class="fields">
|
||||
<div class="sixteen wide field">
|
||||
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" />
|
||||
<input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes and Files" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
112
client/src/components/ShareNoteComponent.vue
Normal file
112
client/src/components/ShareNoteComponent.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div class="ui grid" v-if="this.shareUsername == null">
|
||||
|
||||
<div class="row">
|
||||
<div class="eight wide column">
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<input type="text" placeholder="Share with someone" v-model="shareUserInput" v-on:keyup="onKeyup">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<div class="ui disabled button" v-if="shareUserInput.length == 0">
|
||||
Share
|
||||
</div>
|
||||
<div class="ui green button" v-if="shareUserInput.length > 0" v-on:click="onSubmitClick">
|
||||
Share
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sixteen wide column" v-if="sharedWithUsers.length > 0">
|
||||
<h3>Users who can edit this note</h3>
|
||||
</div>
|
||||
<div class="row" v-for="item in sharedWithUsers">
|
||||
<div class="eight wide middle aligned column">
|
||||
<h3><i class="green user circle icon"></i>{{item.username}}</h3>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<div class="ui basic compact button" v-on:click="onRevokeAccess(item.noteId)">Remove Access</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="ui grid" v-if="this.shareUsername != null">
|
||||
<div class="sixteen wide column">
|
||||
Shared with you by <h3><i class="green user circle icon"></i>{{shareUsername}}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'ShareNoteComponent',
|
||||
props: [ 'noteId', 'rawTextId', 'shareUsername' ],
|
||||
data () {
|
||||
return {
|
||||
sharedWithUsers: [],
|
||||
shareUserInput: '',
|
||||
debounce: null,
|
||||
enableSubmitShare: false,
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
},
|
||||
mounted(){
|
||||
|
||||
if(this.shareUsername == null){
|
||||
this.loadShareList()
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
loadShareList(){
|
||||
axios.post('/api/note/getshareusers', {'rawTextId':this.rawTextId })
|
||||
.then( ({data}) => {
|
||||
this.sharedWithUsers = data
|
||||
})
|
||||
},
|
||||
onRevokeAccess(noteId){
|
||||
axios.post('/api/note/shareremoveuser', {'noteId':noteId})
|
||||
.then( ({data}) => {
|
||||
console.log(data)
|
||||
if(data == true){
|
||||
this.loadShareList()
|
||||
}
|
||||
})
|
||||
},
|
||||
onKeyup(event){
|
||||
if(event.keyCode == 13){
|
||||
this.onSubmitClick()
|
||||
return
|
||||
}
|
||||
},
|
||||
onSubmitClick(){
|
||||
|
||||
axios.post('/api/note/shareadduser', {'noteId':this.noteId, 'rawTextId':this.rawTextId, 'username':this.shareUserInput })
|
||||
.then( ({data}) => {
|
||||
if(data == true){
|
||||
this.shareUserInput = ''
|
||||
this.loadShareList()
|
||||
} else {
|
||||
this.$bus.$emit('notification', 'User not found')
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
133
client/src/components/SideSlideMenuComponent.vue
Normal file
133
client/src/components/SideSlideMenuComponent.vue
Normal file
@ -0,0 +1,133 @@
|
||||
<style type="text/css" scoped>
|
||||
.slide-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 55%;
|
||||
bottom: 0;
|
||||
z-index: 400;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
.slide-content {
|
||||
box-sizing: border-box;
|
||||
/*padding: 1em 1.5em;*/
|
||||
height: calc(100% - 43px);
|
||||
border-right: 1px solid var(--menu-border);
|
||||
background-color: var(--background_color);
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.slide-shadow {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 50%;
|
||||
bottom: 0;
|
||||
color: red;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
/*background: linear-gradient(90deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 55%);*/
|
||||
z-index: 399;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
.note-menu {
|
||||
height: 43px;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 740px) {
|
||||
.slide-shadow {
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
.slide-content {
|
||||
height: calc(100% - 55px);
|
||||
}
|
||||
.slide-container {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.note-menu {
|
||||
height: 55px;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.modal-fade-enter,
|
||||
.modal-fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity .5s ease;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<transition name="modal-fade">
|
||||
<div>
|
||||
|
||||
<div class="slide-container">
|
||||
|
||||
<!-- content of the editor -->
|
||||
<div class="slide-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<!-- close menu on bottom -->
|
||||
<div class="note-menu">
|
||||
<nm-button more-class="right" icon="close" text="close" :show-text="true" v-on:click.native="close" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="slide-shadow" v-on:click="close"></div>
|
||||
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SideSlideMenu',
|
||||
props: [ 'name' ],
|
||||
components: {
|
||||
'nm-button':require('@/components/NoteMenuButtonComponent.vue').default
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
items: []
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
//Other panels will tell this one to close
|
||||
this.$bus.$on('destroy_all_other_side_panels', (name) => {
|
||||
if(this.name != name){
|
||||
this.close()
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeDestroy(){
|
||||
},
|
||||
mounted(){
|
||||
|
||||
//Close all other panels that are not this one
|
||||
this.$nextTick( () => {
|
||||
this.$bus.$emit('destroy_all_other_side_panels', this.name)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
onClickTag(index){
|
||||
console.log('yup')
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
119
client/src/components/SimpleAttachmentNoteComponent.vue
Normal file
119
client/src/components/SimpleAttachmentNoteComponent.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<style type="text/css" scoped>
|
||||
|
||||
.img-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.img-row {
|
||||
height: 30vh;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.img-row:last-child {
|
||||
/* There's no science in using "10" here. In all my testing, this delivered the best results. */
|
||||
flex-grow: 10;
|
||||
}
|
||||
|
||||
.img-row > img {
|
||||
max-height: calc(100% - 10px);
|
||||
min-width: calc(100% - 10px);
|
||||
max-width: calc(100% - 10px);
|
||||
object-fit: cover;
|
||||
vertical-align: bottom;
|
||||
/*padding: 5px;*/
|
||||
box-shadow: 0px 2px 2px 1px rgba(34,36,38,0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div v-if="uploadedToNote.length > 0">
|
||||
<h2>Images Uploaded to Note</h2>
|
||||
<div class="ui fluid green button" v-on:click="$router.push('/attachments/note/'+noteId)">
|
||||
Manage Files on this Note
|
||||
<i class="chevron circle right icon"></i>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="img-container">
|
||||
<div v-for="file in uploadedToNote" class="img-row" v-on:click="onFileClick(file)">
|
||||
<img :src="`/api/static/thumb_${file.file_location}`">
|
||||
</div>
|
||||
|
||||
<!-- extra row helps it display properly -->
|
||||
<div class="img-row"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Add images to note -->
|
||||
<div v-if="files.length > 0">
|
||||
<h2>All other Images</h2>
|
||||
<div class="ui fluid green button" v-on:click="$router.push('/attachments')">
|
||||
Manage All Files
|
||||
<i class="chevron circle right icon"></i>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="img-container">
|
||||
<div v-for="file in files" class="img-row" v-on:click="onFileClick(file)">
|
||||
<img :src="`/api/static/thumb_${file.file_location}`">
|
||||
</div>
|
||||
|
||||
<!-- extra row helps it display properly -->
|
||||
<div class="img-row"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'SimpleAttachmentNoteComponent',
|
||||
props: [ 'noteId', 'squireEditor' ],
|
||||
data () {
|
||||
return {
|
||||
files: [],
|
||||
uploadedToNote: [],
|
||||
}
|
||||
},
|
||||
beforeMount(){
|
||||
|
||||
},
|
||||
mounted(){
|
||||
axios.post('/api/attachment/search', {'attachmentType':2})
|
||||
.then( ({data}) => {
|
||||
|
||||
//Sort files into two categories
|
||||
data.forEach(file => {
|
||||
if(file['note_id'] == this.noteId){
|
||||
this.uploadedToNote.push(file)
|
||||
} else {
|
||||
this.files.push(file)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
onFileClick(file){
|
||||
|
||||
const imageCode = `<img alt="image" src="/api/static/thumb_${file.file_location}">`
|
||||
|
||||
this.$bus.$emit('new_file_upload', {noteId: this.noteId, imageCode})
|
||||
|
||||
if(this.$store.getters.getIsUserOnMobile){
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
@ -71,11 +71,12 @@
|
||||
//Redirect user to notes section after login
|
||||
vm.$router.push('/notes')
|
||||
} else {
|
||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
||||
vm.$store.commit('destroyLoginToken')
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('There was an error with log in request')
|
||||
this.$bus.$emit('notification', 'Incorrect Username or Password')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -11,13 +11,12 @@
|
||||
<div class="fields">
|
||||
<div class="ten wide field">
|
||||
<search-input></search-input>
|
||||
<!-- <input v-model="searchTerm" @keyup="searchKeyUp" @:keyup.enter="search" placeholder="Search Notes" /> -->
|
||||
</div>
|
||||
<div class="six wide field">
|
||||
<span class="ui fluid green button"
|
||||
v-if="showClear"
|
||||
@click="reset">
|
||||
<i class="undo icon"></i>Reset Filters
|
||||
<i class="undo icon"></i>Back to All Notes
|
||||
</span>
|
||||
<!-- <fast-filters /> -->
|
||||
</div>
|
||||
@ -36,6 +35,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="commonTags.length > 0" class="sixteen wide column">
|
||||
<h4><i class="green tags icon"></i>Tags</h4>
|
||||
<span v-for="tag in commonTags" @click="toggleTagFilter(tag.id)">
|
||||
<span class="ui clickable basic label" :class="{ 'green':(searchTags.includes(tag.id)) }">
|
||||
{{ucWords(tag.text)}} <span class="detail">{{tag.usages}}</span>
|
||||
@ -388,6 +388,7 @@
|
||||
},
|
||||
//Try to close notes on URL hash change /notes/open/123 to /notes - parse 123, close note id 123
|
||||
hashChangeAction(event){
|
||||
|
||||
//Clean up path of hash change
|
||||
let path = window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.hash
|
||||
let newPath = event.newURL.replace(path,'')
|
||||
|
@ -43,7 +43,7 @@ Attachment.textSearch = (userId, searchTerm) => {
|
||||
})
|
||||
}
|
||||
|
||||
Attachment.search = (userId, noteId) => {
|
||||
Attachment.search = (userId, noteId, attachmentType) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let params = [userId]
|
||||
@ -54,6 +54,11 @@ Attachment.search = (userId, noteId) => {
|
||||
params.push(noteId)
|
||||
}
|
||||
|
||||
if(Number.isInteger(attachmentType)){
|
||||
query += 'AND attachment_type = ? '
|
||||
params.push(attachmentType)
|
||||
}
|
||||
|
||||
query += 'ORDER BY last_indexed DESC '
|
||||
|
||||
db.promise()
|
||||
|
@ -14,6 +14,35 @@ let Note = module.exports = {}
|
||||
|
||||
const gm = require('gm')
|
||||
|
||||
// --------------
|
||||
|
||||
Note.migrateNoteTextToNewTable = () => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query('SELECT id, text FROM note WHERE note_raw_text_id IS NULL')
|
||||
.then((rows, fields) => {
|
||||
rows[0].forEach( ({id, text}) => {
|
||||
|
||||
db.promise()
|
||||
.query('INSERT INTO note_raw_text (text) VALUES (?)', [text])
|
||||
.then((rows, fields) => {
|
||||
|
||||
db.promise()
|
||||
.query(`UPDATE note SET note_raw_text_id = ? WHERE (id = ?)`, [rows[0].insertId, id])
|
||||
.then((rows, fields) => {
|
||||
|
||||
return 'Nice'
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
resolve('Its probably running... :-D')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Note.fixAttachmentThumbnails = () => {
|
||||
const filePath = '../staticFiles/'
|
||||
db.promise()
|
||||
@ -66,6 +95,8 @@ Note.stressTest = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// --------------
|
||||
|
||||
Note.create = (userId, noteText, quickNote = 0) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@ -74,34 +105,18 @@ Note.create = (userId, noteText, quickNote = 0) => {
|
||||
const created = Math.round((+new Date)/1000)
|
||||
|
||||
db.promise()
|
||||
.query('INSERT INTO note (user_id, text, updated, created, quick_note) VALUES (?,?,?,?,?)',
|
||||
[userId, noteText, created, created, quickNote])
|
||||
.then((rows, fields) => {
|
||||
// New notes are empty, don't add to solr index
|
||||
// Note.reindex(userId, rows[0].insertId)
|
||||
resolve(rows[0].insertId) //Only return the new note ID when creating a new note
|
||||
.query(`INSERT INTO note_raw_text (text) VALUE ('')`)
|
||||
.then( (rows, fields) => {
|
||||
|
||||
const rawTextId = rows[0].insertId
|
||||
|
||||
return db.promise()
|
||||
.query('INSERT INTO note (user_id, note_raw_text_id, updated, created, quick_note) VALUES (?,?,?,?,?)',
|
||||
[userId, rawTextId, created, created, quickNote])
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
Note.reindexAll = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
db.promise()
|
||||
.query(`
|
||||
|
||||
SELECT id, user_id FROM note;
|
||||
|
||||
`)
|
||||
.then((rows, fields) => {
|
||||
console.log()
|
||||
|
||||
rows[0].forEach(item => {
|
||||
Note.reindex(item.user_id, item.id)
|
||||
})
|
||||
|
||||
resolve(true)
|
||||
// Indexing is done on save
|
||||
resolve(rows[0].insertId) //Only return the new note ID when creating a new note
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
@ -154,8 +169,23 @@ Note.update = (userId, noteId, noteText, color, pinned, archived) => {
|
||||
const now = Math.round((+new Date)/1000)
|
||||
|
||||
db.promise()
|
||||
.query('UPDATE note SET text = ?, pinned = ?, archived = ?, updated = ?, color = ? WHERE id = ? AND user_id = ? LIMIT 1',
|
||||
[noteText, pinned, archived, now, color, noteId, userId])
|
||||
.query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
const textId = rows[0][0]['note_raw_text_id']
|
||||
|
||||
//Update Note text
|
||||
return db.promise()
|
||||
.query('UPDATE note_raw_text SET text = ? WHERE id = ?', [noteText, textId])
|
||||
})
|
||||
.then( (rows, fields) => {
|
||||
|
||||
//Update other note attributes
|
||||
return db.promise()
|
||||
.query('UPDATE note SET pinned = ?, archived = ?, updated = ?, color = ? WHERE id = ? AND user_id = ? LIMIT 1',
|
||||
[pinned, archived, now, color, noteId, userId])
|
||||
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Async solr note reindex
|
||||
@ -171,7 +201,6 @@ Note.update = (userId, noteId, noteText, color, pinned, archived) => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Delete a note and all its remaining parts
|
||||
//
|
||||
@ -179,16 +208,54 @@ Note.delete = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
//
|
||||
// Delete, note, text index, tags
|
||||
// Delete, note, text, search index and associated tags
|
||||
// Leave the attachments, they can be deleted on their own
|
||||
//
|
||||
// Leave Tags, their text is shared
|
||||
|
||||
db.promise().query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId,userId])
|
||||
let rawTextId = null
|
||||
let noteTextCount = 0
|
||||
|
||||
// Lookup the note text ID, we need this to count usages
|
||||
db.promise()
|
||||
.query('SELECT note_raw_text_id FROM note WHERE id = ? AND user_id = ?', [noteId, userId])
|
||||
.then((rows, fields) => {
|
||||
return db.promise().query('DELETE FROM note_text_index WHERE note_text_index.note_id = ? AND note_text_index.user_id = ?', [noteId,userId])
|
||||
|
||||
//Save the raw text ID
|
||||
rawTextId = rows[0][0]['note_raw_text_id']
|
||||
|
||||
return db.promise()
|
||||
.query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
return db.promise().query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
|
||||
|
||||
//Save the number of times the note is used
|
||||
noteTextCount = rows[0][0]['count']
|
||||
|
||||
//Don't delete text if its shared
|
||||
if(noteTextCount == 1){
|
||||
//If text is only used on one note, delete it (its not shared)
|
||||
return db.promise()
|
||||
.query('SELECT count(id) as count FROM note WHERE note_raw_text_id = ?', [rawTextId])
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(true)
|
||||
})
|
||||
}
|
||||
})
|
||||
.then( results => {
|
||||
// Delete Note entry for this user.
|
||||
return db.promise()
|
||||
.query('DELETE FROM note WHERE note.id = ? AND note.user_id = ?', [noteId, userId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
// Delete search index
|
||||
return db.promise()
|
||||
.query('DELETE FROM note_text_index WHERE note_text_index.note_id = ? AND note_text_index.user_id = ?', [noteId,userId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
// delete tags
|
||||
return db.promise()
|
||||
.query('DELETE FROM note_tag WHERE note_tag.note_id = ? AND note_tag.user_id = ?', [noteId,userId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
resolve(true)
|
||||
@ -251,9 +318,19 @@ Note.get = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query(`
|
||||
SELECT note.text, note.updated, note.pinned, note.archived, note.color, count(distinct attachment.id) as attachment_count
|
||||
SELECT
|
||||
note_raw_text.text,
|
||||
note.updated,
|
||||
note.pinned,
|
||||
note.archived,
|
||||
note.color,
|
||||
count(distinct attachment.id) as attachment_count,
|
||||
note.note_raw_text_id as rawTextId,
|
||||
user.username as shareUsername
|
||||
FROM note
|
||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
LEFT JOIN attachment ON (note.id = attachment.note_id)
|
||||
LEFT JOIN user ON (note.share_user_id = user.id)
|
||||
WHERE note.user_id = ? AND note.id = ? LIMIT 1`, [userId,noteId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
@ -265,6 +342,7 @@ Note.get = (userId, noteId) => {
|
||||
})
|
||||
}
|
||||
|
||||
//Public note share action -> may not be used
|
||||
Note.getShared = (noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
@ -356,20 +434,33 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
|
||||
|
||||
// Base of the query, modified with fastFilters
|
||||
// Add to query for character counts -> CHAR_LENGTH(note.text) as chars
|
||||
let searchParams = [userId]
|
||||
let noteSearchQuery = `
|
||||
SELECT note.id,
|
||||
SUBSTRING(note.text, 1, 1500) as text,
|
||||
SUBSTRING(note_raw_text.text, 1, 1500) as text,
|
||||
updated,
|
||||
color,
|
||||
count(distinct note_tag.id) as tag_count,
|
||||
count(distinct attachment.id) as attachment_count,
|
||||
note.pinned,
|
||||
note.archived
|
||||
note.archived,
|
||||
GROUP_CONCAT(DISTINCT tag.text) as tags,
|
||||
GROUP_CONCAT(DISTINCT attachment.file_location) as thumbs,
|
||||
shareUser.username as username
|
||||
FROM note
|
||||
JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id)
|
||||
LEFT JOIN note_tag ON (note.id = note_tag.note_id)
|
||||
LEFT JOIN tag ON (tag.id = note_tag.tag_id)
|
||||
LEFT JOIN attachment ON (note.id = attachment.note_id)
|
||||
LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id)
|
||||
WHERE note.user_id = ?`
|
||||
let searchParams = [userId]
|
||||
|
||||
//Show shared notes
|
||||
if(fastFilters.onlyShowSharedNotes == 1){
|
||||
noteSearchQuery += ' AND note.share_user_id IS NOT NULL' //Show Archived
|
||||
} else {
|
||||
noteSearchQuery += ' AND note.share_user_id IS NULL'
|
||||
}
|
||||
|
||||
//If text search returned results, limit search to those ids
|
||||
if(textSearchIds.length > 0){
|
||||
@ -418,15 +509,15 @@ Note.search = (userId, searchQuery, searchTags, fastFilters) => {
|
||||
// Always prioritize pinned notes in searches.
|
||||
|
||||
//Default Sort, order by last updated
|
||||
let defaultOrderBy = ' ORDER BY note.pinned DESC, updated DESC, created DESC, opened DESC, id DESC'
|
||||
let defaultOrderBy = ' ORDER BY note.pinned DESC, note.updated DESC, note.created DESC, note.opened DESC, id DESC'
|
||||
|
||||
//Order by Last Created Date
|
||||
if(fastFilters.lastCreated == 1){
|
||||
defaultOrderBy = ' ORDER BY note.pinned DESC, created DESC, updated DESC, opened DESC, id DESC'
|
||||
defaultOrderBy = ' ORDER BY note.pinned DESC, note.created DESC, note.updated DESC, note.opened DESC, id DESC'
|
||||
}
|
||||
//Order by last Opened Date
|
||||
if(fastFilters.lastOpened == 1){
|
||||
defaultOrderBy = ' ORDER BY note.pinned DESC, opened DESC, updated DESC, created DESC, id DESC'
|
||||
defaultOrderBy = ' ORDER BY note.pinned DESC, opened DESC, note.updated DESC, note.created DESC, id DESC'
|
||||
}
|
||||
|
||||
//Append Order by to query
|
||||
|
127
server/models/ShareNote.js
Normal file
127
server/models/ShareNote.js
Normal file
@ -0,0 +1,127 @@
|
||||
//
|
||||
// All actions in noteController.js
|
||||
//
|
||||
|
||||
|
||||
const db = require('@config/database')
|
||||
|
||||
const Note = require('@models/Note')
|
||||
|
||||
let ShareNote = module.exports = {}
|
||||
|
||||
// Share a note with a user, given the correct username
|
||||
ShareNote.addUser = (userId, noteId, rawTextId, username) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
let shareUserId = null
|
||||
let newNoteShare = null
|
||||
|
||||
//Check that user actually exists
|
||||
db.promise().query(`SELECT id FROM user WHERE username = ?`, [username])
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(rows[0].length == 0){
|
||||
throw new Error('User Does Not Exist')
|
||||
}
|
||||
|
||||
shareUserId = rows[0][0]['id']
|
||||
|
||||
//Check if note has already been added for user
|
||||
return db.promise()
|
||||
.query('SELECT id FROM note WHERE user_id = ? AND note_raw_text_id = ?', [shareUserId, rawTextId])
|
||||
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
if(rows[0].length != 0){
|
||||
throw new Error('User Already Has Note')
|
||||
}
|
||||
|
||||
//Lookup note to share with user, clone this data to create users new note
|
||||
return db.promise()
|
||||
.query(`SELECT * FROM note WHERE id = ? LIMIT 1`, [noteId])
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
newNoteShare = rows[0][0]
|
||||
|
||||
//Modify note with the share attributes we want
|
||||
delete newNoteShare['id']
|
||||
delete newNoteShare['opened']
|
||||
newNoteShare['share_user_id'] = userId //User who shared the note
|
||||
newNoteShare['user_id'] = shareUserId //User who gets note
|
||||
|
||||
//Setup db colums, db values and number of '?' to put into prepared statement
|
||||
let dbColumns = []
|
||||
let dbValues = []
|
||||
let escapeChars = []
|
||||
|
||||
//Pull out all the data we need from object to create prepared statemnt
|
||||
Object.keys(newNoteShare).forEach( key => {
|
||||
escapeChars.push('?')
|
||||
dbColumns.push(key)
|
||||
dbValues.push(newNoteShare[key])
|
||||
})
|
||||
|
||||
//Stick all the note value back into query, insert updated note
|
||||
return db.promise()
|
||||
.query(`INSERT INTO note (${dbColumns.join()}) VALUES (${escapeChars.join()})`, dbValues)
|
||||
})
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Success!
|
||||
return resolve(true)
|
||||
})
|
||||
.catch(error => {
|
||||
// console.log(error)
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Get users who see a shared note
|
||||
ShareNote.getUsers = (userId, rawTextId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.promise()
|
||||
.query(`
|
||||
SELECT username, note.id as noteId
|
||||
FROM note
|
||||
JOIN user ON (user.id = note.user_id)
|
||||
WHERE note_raw_text_id = ?
|
||||
AND share_user_id = ?
|
||||
AND user_id != ?
|
||||
`, [rawTextId, userId, userId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
//Return a list of user names
|
||||
return resolve (rows[0])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Remove a user from a shared note
|
||||
ShareNote.removeUser = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
//note.id = noteId, share_user_id = userId
|
||||
db.promise()
|
||||
.query('SELECT user_id FROM note WHERE id = ? AND share_user_id = ?', [noteId, userId])
|
||||
.then( (rows, fields) => {
|
||||
|
||||
//User has shared this note, with this user
|
||||
if(rows[0].length == 1 && Number.isInteger(rows[0][0]['user_id'])){
|
||||
|
||||
Note.delete(rows[0][0]['user_id'], noteId)
|
||||
.then( result => {
|
||||
resolve(result)
|
||||
})
|
||||
|
||||
} else {
|
||||
return resolve(false)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
}
|
@ -10,7 +10,7 @@ Tag.userTags = (userId) => {
|
||||
JOIN note_tag ON tag.id = note_tag.tag_id
|
||||
WHERE note_tag.user_id = ?
|
||||
GROUP BY tag.id
|
||||
ORDER BY usages DESC
|
||||
ORDER BY id DESC
|
||||
`, [userId])
|
||||
.then( (rows, fields) => {
|
||||
resolve(rows[0])
|
||||
@ -98,39 +98,50 @@ Tag.add = (tagText) => {
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Get all tags AND tags associated to note
|
||||
//
|
||||
Tag.get = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
//Update last opened date of note
|
||||
const now = Math.round((+new Date)/1000)
|
||||
db.promise()
|
||||
.query('UPDATE note SET opened = ? WHERE id = ? AND user_id = ? LIMIT 1', [now, noteId, userId])
|
||||
.then((rows, fields) => {})
|
||||
Tag.userTags(userId).then(userTags => {
|
||||
db.promise()
|
||||
.query(`SELECT tag_id as tagId, id as entryId
|
||||
FROM note_tag
|
||||
WHERE user_id = ? AND note_id = ?;`, [userId, noteId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
db.promise()
|
||||
.query(`SELECT note_tag.id, tag.text FROM note_tag
|
||||
JOIN tag ON (tag.id = note_tag.tag_id)
|
||||
WHERE user_id = ? AND note_id = ?;`, [userId, noteId])
|
||||
.then((rows, fields) => {
|
||||
resolve(rows[0]) //Return all tags found by query
|
||||
//pull IDs out of returned results
|
||||
// let ids = rows[0].map( item => {})
|
||||
|
||||
resolve({'noteTagIds':rows[0], 'allTags':userTags }) //Return all tags found by query
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
.catch(console.log)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Get all tags for a note and concatinate into a string 'all, tags, like, this'
|
||||
//
|
||||
Tag.string = (userId, noteId) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Tag.get(userId, noteId).then(tagArray => {
|
||||
db.promise()
|
||||
.query(`SELECT GROUP_CONCAT(DISTINCT tag.text SEPARATOR ', ') as text
|
||||
FROM note_tag
|
||||
JOIN tag ON note_tag.tag_id = tag.id
|
||||
WHERE user_id = ? AND note_id = ?;`, [userId, noteId])
|
||||
.then((rows, fields) => {
|
||||
|
||||
let tagString = ''
|
||||
tagArray.forEach( (tag, i) => {
|
||||
if(i > 0){ tagString += ',' }
|
||||
tagString += tag.text
|
||||
})
|
||||
//Output comma delimited list of tag strings
|
||||
resolve(tagString)
|
||||
let finalText = rows[0][0]['text']
|
||||
if(finalText == null){
|
||||
finalText = ''
|
||||
}
|
||||
|
||||
return resolve(finalText) //Return all tags found by query
|
||||
})
|
||||
.catch(console.log)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ router.use(function setUserId (req, res, next) {
|
||||
})
|
||||
|
||||
router.post('/search', function (req, res) {
|
||||
Attachment.search(userId, req.body.noteId)
|
||||
Attachment.search(userId, req.body.noteId, req.body.attachmentType)
|
||||
.then( data => res.send(data) )
|
||||
})
|
||||
|
||||
|
@ -2,6 +2,7 @@ var express = require('express')
|
||||
var router = express.Router()
|
||||
|
||||
let Notes = require('@models/Note');
|
||||
let ShareNote = require('@models/ShareNote');
|
||||
|
||||
let userId = null
|
||||
let socket = null
|
||||
@ -18,6 +19,9 @@ router.use(function setUserId (req, res, next) {
|
||||
next()
|
||||
})
|
||||
|
||||
//
|
||||
// Note actions
|
||||
//
|
||||
router.post('/get', function (req, res) {
|
||||
req.io.emit('welcome_homie', 'Welcome, dont poop from excitement')
|
||||
Notes.get(userId, req.body.noteId)
|
||||
@ -58,15 +62,35 @@ router.post('/difftext', function (req, res) {
|
||||
})
|
||||
})
|
||||
|
||||
//
|
||||
// Share Note Actions
|
||||
//
|
||||
router.post('/getshareusers', function (req, res) {
|
||||
ShareNote.getUsers(userId, req.body.rawTextId)
|
||||
.then(results => res.send(results))
|
||||
})
|
||||
|
||||
router.post('/shareadduser', function (req, res) {
|
||||
ShareNote.addUser(userId, req.body.noteId, req.body.rawTextId, req.body.username)
|
||||
.then(results => res.send(results))
|
||||
})
|
||||
|
||||
router.post('/shareremoveuser', function (req, res) {
|
||||
ShareNote.removeUser(userId, req.body.noteId)
|
||||
.then(results => res.send(results))
|
||||
})
|
||||
|
||||
|
||||
//
|
||||
// Testing Action
|
||||
//
|
||||
//Reindex all notes. Not a very good function, not public
|
||||
router.get('/reindex5yu43prchuj903mrc', function (req, res) {
|
||||
|
||||
Notes.fixAttachmentThumbnails()
|
||||
res.send('A whole mess is going on in the background')
|
||||
Notes.migrateNoteTextToNewTable().then(status => {
|
||||
return res.send(status)
|
||||
})
|
||||
|
||||
// Notes.stressTest().then( i => {
|
||||
// // Notes.reindexAll().then( result => res.send('Welcome to reindex...oh god'))
|
||||
// })
|
||||
})
|
||||
|
||||
|
||||
|
4
staticFiles/.gitignore
vendored
Normal file
4
staticFiles/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
*/
|
||||
!.gitignore
|
||||
!assets
|
Loading…
Reference in New Issue
Block a user