SolidScribe/client/src/components/NoteInputPanel.vue

571 lines
15 KiB
Vue
Raw Normal View History

<template>
<!-- change class to .master-note-edit to have it popup on the screen -->
<div
id="InputNotes"
class="master-note-edit"
@keyup.esc="close"
:class="[{'size-down':(sizeDown == true)}, 'position-'+position ]"
:style="{ 'background-color':styleObject['noteBackground'], 'color':styleObject['noteText']}"
>
<!-- Loading indicator -->
<div v-if="loading" class="loading-note">
<div class="ui active dimmer">
<div class="ui text loader">{{loadingMessage}}</div>
</div>
</div>
<span class="note-status-indicator" v-on:click="save()">{{statusText}}</span>
<div class="tinymce-container">
<textarea :id="noteid+'-tinymce-editor'">{{noteText}}</textarea>
</div>
<color-picker
v-if="colorPickerVisible"
:location="colorPickerLocation"
@changeColor="onChangeColor"
:style-object="styleObject"
/>
<div v-if="$store.getters.getIsNoteSettingsOpen">
<div class="all-settings">
<div class="ui grid">
<div class="row">
<div class="sixteen wide column">
<note-tag-edit :noteId="noteid" :key="'tags-for-note-'+noteid"/><br>
<!-- close button -->
<div class="ui small compact basic icon button" v-on:click="$store.commit('toggleNoteSettingsPane')">
<i class="close icon"></i> Close Settings
</div>
<!-- Pin Button -->
<div @click="onToggleArchived" class="ui small compact basic icon button">
<i class="archive icon" :class="{green:(archived == 1)}"></i>
{{(archived == 1)?'Archived':'Archive'}}
</div>
<!-- archive button -->
<div @click="onTogglePinned" class="ui small compact basic icon button">
<i class="pin icon" :class="{green:(pinned == 1)}"></i>
{{(pinned == 1)?'Pinned':'Pin'}}
</div>
<!-- colors button -->
<span class="relative" v-on:click="showColorPicker">
<span class="ui small compact basic icon button">
<i class="paint brush icon"></i>
Colors
</span>
</span>
<!-- attachment button -->
<div v-if="attachmentCount > 0" class="ui small compact basic icon button" v-on:click="openEditAttachment">
<i class="folder icon"></i> Attachments
</div>
<!-- file upload button -->
<file-upload-button :noteId="noteid" />
</div>
</div>
</div>
</div>
<!-- <div class="shade" v-on:click="showAllSettings = false"></div> -->
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
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,
},
data(){
return {
loading: true,
loadingMessage: 'Loading Note',
currentNoteId: 0,
noteText: '',
statusText: 'Saved',
lastNoteHash: null,
saveDebounce: null, //Prevent save from being called numerous times quickly
updated: 'Never',
editDebounce: null,
keyPressesCounter: 0, //Determen keys pressed between saves
pinned: 0,
archived: 0,
attachmentCount: 0,
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
//Settings vars
showAllSettings: true,
lastVisibilityState: null,
}
},
watch: {
noteid:function(newVal, oldVal){
if(newVal == this.currentNoteId){
return
}
if(newVal == oldVal){
return
}
this.currentNoteId = newVal
this.loadNote(this.currentNoteId)
}
},
beforeMount(){
//Load tinymce into the page only do it once
if(document.querySelectorAll('[data-mceload]').length == 0){
let tinyMceIncluder = document.createElement('script')
tinyMceIncluder.setAttribute('src', '/api/static/assets/tinymce/tinymce.min.js')
tinyMceIncluder.setAttribute('data-mceload','loaded')
document.head.appendChild(tinyMceIncluder)
}
},
beforeDestroy(){
document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
this.$off() // Remove all event listeners
this.$bus.$off()
//Trash editor instance on close
this.tinymce.remove()
},
mounted: function() {
//Change TinyMce styles on nightmored change
this.$bus.$on('toggle_night_mode', this.setEditorTextColor )
document.addEventListener('visibilitychange', this.checkForUpdatedNote)
this.$nextTick(() => {
this.loadNote(this.noteid)
})
},
methods: {
initTinyMce(){
// image_list: [
// {title: 'My image 1', value: 'https://www.tinymce.com/my1.gif'},
// {title: 'My image 2', value: 'http://www.moxiecode.com/my2.gif'}
// ]
//Default plugin options for big browsers
let toolbarOptions = 'customCloseButton | mceTogglePinned | forecolor styleselect | bold italic underline | link | bullist numlist | outdent indent table | h | image'
let pluginOptions = 'paste, link, code, lists, table, hr, image'
//Tweak doc height for mobile
let docHeight = 'calc(100vh - 1px)'
if(this.$store.getters.getIsUserOnMobile){
docHeight = 'calc(100vh - 1px)'
toolbarOptions = 'customCloseButton | bullist numlist | mceTogglePinned'
pluginOptions = 'lists'
}
//setup skin as dark if night mode is enabled
let skin = 'oxide'
if(this.$store.getters.getIsNightMode){
skin = 'oxide-dark'
}
const editorId = '#'+this.noteid+'-tinymce-editor'
let vm = this
//Globally defined included in index HTML
tinymce.init({
selector: editorId,
toolbar: toolbarOptions,
plugins: pluginOptions,
browser_spellcheck: true,
menubar: false,
branding: false,
statusbar: false,
height: docHeight,
skin: skin,
contextmenu: false,
init_instance_callback: this.editorInitCallback,
imagetools_toolbar: "imageoptions",
setup: editor => {
//Add custom buttons to tinymce instance
editor.ui.registry.addButton('customCloseButton', {
text: 'Close',
icon: 'close',
onAction: function (_) {
vm.close()
}
})
//Add custom buttons to tinymce instance
editor.ui.registry.addButton('mceTogglePinned', {
text: 'Note Settings',
icon: 'settings',
onAction: function (_) {
vm.$store.commit('toggleNoteSettingsPane')
}
})
}
})
},
editorInitCallback(editor){
this.loading = false //Turn off loading screed when editor is loaded
this.tinymce = editor
this.setEditorTextColor()
editor
.on('Change', this.onKeyup )
.on('keyup', this.onKeyup )
.on('blur', this.save )
},
setEditorTextColor(){
//Only Set editor text color, background is transparent and set on parent element
//There may be scenarios where editor has not been set up
if(this.tinymce){
//Set editor color to color from app, change with night mode
this.tinymce.getBody().style.color = getComputedStyle(document.documentElement)
.getPropertyValue('--text_color');
//Overwrite set color if theme is set for note.
if(this.styleObject && this.styleObject.noteText){
this.tinymce.getBody().style.color = this.styleObject.noteText
}
}
},
setText(inText){
return this.tinymce.setContent(inText)
},
getText(){
//Return text from tinyMce Editor
return this.tinymce.getContent()
},
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 {
this.pinned = 0;
}
//Update last note hash, this will tell note to save next update
this.lastNoteHash = 0
this.save()
},
onToggleArchived(){
if(this.archived == 0){
this.archived = 1
} else {
this.archived = 0;
}
//Update last note hash, this will tell note to save next update
this.lastNoteHash = 0
this.save()
},
onChangeColor(newStyleObject){
//Set new style object for note, page will use some styles, styles will be saved to database
this.styleObject = newStyleObject
this.setEditorTextColor()
this.lastNoteHash = 0 //Update hash to force note update on next save
this.save()
},
loadNote(noteId){
let vm = this
let doing = ['Loading','Loading','Getting','Fetching','Grabbing','Sequencing','Organizing','Untangling','Processing','Refining','Extracting','Fusing','Pruning','Expanding','Enlarging','Transfiguring','Quantizing','Ingratiating','Lumping']
let thing = ['Note','Note','Note','Note','Data','Text','Document','Algorithm','Buffer','Client','Download','File','Frame','Graphics','Hardware','HTML','Interface','Logic','Mainframe','Memory','Media','Nodes','Network','Chaos']
let p1 = doing[Math.floor(Math.random() * doing.length)]
let p2 = thing[Math.floor(Math.random() * thing.length)]
vm.loadingMessage = p1 + ' ' + p2
//Component is activated with NoteId in place, lookup text with associated ID
if(this.$store.getters.getLoggedIn){
axios.post('/api/note/get', {'noteId': noteId})
.then(response => {
//Set up local data
vm.currentNoteId = noteId
vm.noteText = response.data.text
vm.updated = response.data.updated
vm.lastNoteHash = vm.hashString(response.data.text)
if(response.data.color){
vm.styleObject = JSON.parse(response.data.color) //Load styles json from DB
}
if(response.data.pinned != null){
vm.pinned = response.data.pinned
}
vm.archived = response.data.archived
vm.attachmentCount = response.data.attachment_count
vm.$nextTick(() => {
this.initTinyMce()
})
})
} else {
console.log('Could not fetch note')
}
},
onKeyup(){
this.statusText = 'Modified'
//Each note, save after 5 seconds, focus lost or 30 characters typed.
clearTimeout(this.editDebounce)
this.editDebounce = setTimeout(() => {
this.save()
}, 5000)
//Save after 30 keystrokes
this.keyPressesCounter = (this.keyPressesCounter + 1)
if(this.keyPressesCounter > 30){
this.keyPressesCounter = 0
this.save()
}
},
save(){
return new Promise((resolve, reject) => {
//Clear other debounced events to prevent double calling of save
// clearTimeout(this.editDebounce)
//Don't save note if its hash doesn't change
const currentNoteText = this.getText()
if( this.lastNoteHash == this.hashString( currentNoteText )){
this.statusText = 'Saved'
return resolve(true)
}
//If user accidentally clears note, it won't delete it
if(currentNoteText == ''){
this.statusText = 'Empty'
console.log('Prevented from saving empty note.')
return resolve(true)
}
const postData = {
'noteId':this.currentNoteId,
'text': currentNoteText,
'color': JSON.stringify(this.styleObject), //Save little json color object
'pinned': this.pinned,
'archived':this.archived,
}
let vm = this
//Debounce save to prevent spamming
// clearTimeout(this.saveDebounce)
// this.saveDebounce = setTimeout(() => {
//Only notify user if saving - may help with debugging in the future
vm.statusText = 'Saving'
axios.post('/api/note/update', postData).then( response => {
vm.statusText = 'Saved'
vm.updated = Math.round((+new Date)/1000)
//Update last saved note hash
vm.lastNoteHash = vm.hashString( currentNoteText )
return resolve(true)
})
// }, 300)
})
},
checkForUpdatedNote(){
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
console.log('Checking for changes is note data')
const postData = {
noteId:this.currentNoteId,
text:this.getText(),
updated: this.updated
}
axios.post('/api/note/difftext', postData)
.then( ({data}) => {
//Don't do anything if nothing has changed
if(data == ''){ return }
if(data.diffs > 0){
//Update text and last updated time for note
this.setText(data.updatedText)
this.updated = data.updated
}
})
}
//Track visibility state
this.lastVisibilityState = document.visibilityState
},
hashString(text){
var hash = 0;
if (text.length == 0) {
return hash;
}
//Simplified for speed
return text.length
for (let i = 0; i < text.length; i++) {
let char = text.charCodeAt(i);
hash = ((hash<<5)-hash)+char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
},
close(){
// this.loading = true
// this.loadingMessage = 'Save and Close'
this.save().then( result => {
this.sizeDown = true
//This timeout allows animation to play before closing
setTimeout(() => {
this.$bus.$emit('close_active_note', {position: this.position, noteId: this.noteid})
return
}, 300)
})
}
}
}
</script>
<style type="text/css" scoped>
/*Settings manager styles */
.all-settings {
position: absolute;
bottom: -5px;
right: 10px;
left: 10px;
z-index: 99;
border: 1px solid;
background-color: var(--background_color);
border-color: var(--border_color);
box-sizing: border-box;
border-radius: 7px;
box-shadow: 0px 3px 7px 0px rgba(140,140,140,1);
padding: 1em;
}
/*End Settings manager styles */
.tinymce-container {
/* Uncomment this to see the */
/*border-bottom: 2px solid green !important;*/
}
.note-top-menu {
width: 100%;
display: inline-block;
height: 37px;
border-left: 3px solid var(--border_color);
}
.note-top-menu .ui.basic.button {
border-radius: 0;
border: none;
border-right: 1px solid var(--border_color);
margin: 0px -2px;
padding-left: 15px;
padding-right: 15px;
}
/* container styles change based on mobile and number of open screens */
.master-note-edit {
position: fixed;
bottom: 0;
background: var(--background_color);
/*color: var(--text_color);*/
height: 100vh;
box-shadow: 0px 0px 5px 2px rgba(140,140,140,1);
z-index: 1001;
/*overflow-x: scroll;*/
}
.loading-note {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* One note open, in the middle of the screen */
.master-note-edit.position-0 {
left: 50%;
right: 0;
}
@media only screen and (max-width: 740px) {
.master-note-edit.position-0 {
left: 0;
right: 0;
top: 0;
bottom: 0;
}
}
/* Two Notes Open, each takes up 50% of the space */
.master-note-edit.position-1 {
left: 50%;
right: 0%;
}
.master-note-edit.position-2 {
left: 0%;
right: 50%;
}
.size-down {
animation: size-down 0.5s ease;
}
@keyframes size-down {
0% {
/*opacity: 1;*/
/*top: 0;*/
top: 0;
}
100% {
/*opacity: 0;*/
/*top: 30vh;*/
top: 150vh;
}
}
</style>