SolidScribe/client/src/pages/NotesPage.vue
Max 1a6a7bdfd4 Updated vue CLI to latest version
Added cycle tracking base
2022-10-13 19:28:35 +00:00

1053 lines
28 KiB
Vue

<template>
<div class="page-container">
<div class="ui grid" ref="content">
<div class="sixteen wide column">
<!-- :class="{ 'sixteen wide column':showOneColumn 'sixteen wide column':!showOneColumn}" -->
<div class="ui stackable grid">
<div class="six wide column" v-if="$store.getters.totals && $store.getters.totals['totalNotes']">
<search-input />
</div>
<div class="ten wide column" :class="{ 'sixteen wide column':$store.getters.getIsUserOnMobile }">
<div class="ui basic button shrinking"
v-on:click="updateFastFilters(3)"
v-if="$store.getters.totals && ($store.getters.totals['youGotMailCount'] > 0)"
style="position: relative;">
<i class="green mail icon"></i>Inbox
<span class="tiny circular floating ui green label">+{{ $store.getters.totals['youGotMailCount'] }}</span>
</div>
<tag-display
:active-tags="searchTags"
v-on:tagClick="tagId => toggleTagFilter(tagId)"
/>
<paste-button />
<span class="ui grey text text-fix">
Active Sessions {{ $store.getters.getActiveSessions }}
</span>
</div>
<div class="eight wide column" v-if="showClear">
<!-- <fast-filters /> -->
<span class="ui fluid green button" @click="reset">
<i class="arrow circle left icon"></i>Show All Notes
</span>
</div>
</div>
</div>
<div class="sixteen wide column" v-if="searchTerm.length > 0 && !showLoading">
<h2 class="ui header">
<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>
<div v-if="fastFilters['onlyArchived'] == 1" class="sixteen wide column">
<h2>
<i class="green archive icon"></i>
Archived Notes</h2>
</div>
<div class="sixteen wide column" v-if="fastFilters['onlyShowTrashed'] == 1">
<h2>
<i class="green trash alternate outline icon"></i>
Trashed Notes
<span>({{ $store.getters.totals['trashedNotes'] }})</span>
<div class="ui right floated basic button" data-tooltip="This doesn't work yet">
<i class="poo storm icon"></i>
Empty Trash
</div>
</h2>
</div>
<div class="sixteen wide column" v-if="fastFilters['onlyShowSharedNotes'] == 1">
<h2><i class="green paper plane outline icon"></i>
Shared Notes</h2>
</div>
<div class="sixteen wide column" v-if="tagSuggestions.length > 0">
<h5 class="ui tiny dividing header"><i class="green tags icon"></i> Tags ({{ tagSuggestions.length }})</h5>
<div class="ui clickable green label" v-for="tag in tagSuggestions" v-on:click="tagId => toggleTagFilter(tag.id)">
<i class="tag icon"></i>
{{ tag.text }}
</div>
</div>
<!-- Note title card display -->
<div class="sixteen wide column">
<h3 v-if="$store.getters.totals && $store.getters.totals['totalNotes'] == 0 && fastFilters['notesHome'] == 1">
No Notes Yet. <br>Thats ok.<br><br> <br>
<img loading="lazy" width="25%" src="/api/static/assets/marketing/hamburger.svg" alt="Create a new note"><br>
Create one when you feel ready.
</h3>
<!-- Go to one wide column, do not do this on mobile interface -->
<div :class="{'one-column':( showOneColumn), 'floating-list':( isFloatingList ), 'hidden-floating-list':(collapseFloatingList)}" v-on:scroll="onScroll">
<div class="ui basic fitted right aligned segment" v-if="isFloatingList">
<div class="ui small basic green left floated button" v-on:click="closeAllNotes()" v-if="openNotes.length >= 1">
<i class="close icon"></i>
Close Notes
</div>
<div class="ui small green button" v-on:click="collapseFloatingList = true">
<i class="caret square left outline icon"></i>
Hide Menu
</div>
</div>
<!-- render each section based on notes in set -->
<div v-for="section,index in noteSections" v-if="section.length > 0" class="note-card-section">
<h5 class="ui tiny dividing header"><i :class="`green ${sectionData[index][0]} icon`"></i>{{ sectionData[index][1] }}</h5>
<div class="note-card-display-area">
<note-title-display-card
v-on:tagClick="tagId => toggleTagFilter(tagId)"
v-for="note in section"
:ref="'note-'+note.id"
:onClick="openNote"
:data="note"
:title-view="titleView || isFloatingList"
:currently-open="openNotes.includes(note.id)"
:key="note.id + note.color + '-' +note.title.length + '-' +note.subtext.length + '-' + note.tag_count + note.updated"
/>
</div>
</div>
<div class="loading-section" v-if="showLoading">
<loading-icon message="Decrypting Notes" />
</div>
</div>
</div>
<!-- found attachments -->
<div class="sixteen wide column" v-if="foundAttachments.length > 0">
<h5 class="ui tiny dividing header"><i class="green folder open outline icon"></i> Files ({{ foundAttachments.length }})</h5>
<attachment-display
v-for="item in foundAttachments"
:item="item"
:key="item.id"
:search-params="{}"
/>
</div>
</div>
<div class="show-hidden-note-list-button" v-if="collapseFloatingList" v-on:click="collapseFloatingList = false">
<i class="caret square right outline icon"></i>
</div>
<!-- flexbox note container evenly spaces open notes -->
<div class="note-panel-container" :class="{ 'note-panel-fullwidth':collapseFloatingList}" v-if="openNotes.length">
<note-input-panel
v-for="noteId in openNotes"
v-if="noteId != null"
:key="noteId"
:noteid="noteId"
:url-data="$route.params"
:open-notes="openNotes.length"
/>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'NotesPage',
components: {
'note-input-panel': () => import(/* webpackChunkName: "NoteInputPanel" */ '@/components/NoteInputPanel.vue'),
'note-title-display-card': require('@/components/NoteTitleDisplayCard.vue').default,
// 'fast-filters': require('@/components/FastFilters.vue').default,
'search-input': require('@/components/SearchInput.vue').default,
'attachment-display': require('@/components/AttachmentDisplayCard').default,
'tag-display':require('@/components/TagDisplayComponent.vue').default,
'loading-icon':require('@/components/LoadingIconComponent.vue').default,
'paste-button':require('@/components/PasteButton.vue').default,
},
data () {
return {
initComponent: true,
tagSuggestions:[],
searchTerm: '',
searchResultsCount: 0,
searchTags: [],
notes: [],
openNotes: [],
collapseFloatingList: false,
highlights: [],
searchDebounce: null,
fastFilters: {},
titleView: false,
//Load up notes in batches
firstLoadBatchSize: 10, //First set of rapidly loaded notes
batchSize: 20, //Size of batch loaded when user scrolls through current batch
batchOffset: 0, //Tracks the current batch that has been loaded
loadingBatchTimeout: null, //Limit how quickly batches can be loaded
showLoading: false,
scrollLoadEnabled: true,
//Clear button is not visible
showClear: false,
initialPostData: null,
//Currently open notes in app
activeNoteId1: null,
activeNoteId2: null,
//Position determines how note is Positioned
activeNote1Position: 0,
activeNote2Position: 0,
lastVisibilityState: null,
foundAttachments: [],
sectionData: {
'pinned': ['thumbtack', 'Pinned'],
'archived': ['archive', 'Archived'],
'shared': ['envelope outline', 'Inbox'],
'sent': ['paper plane outline', 'Sent Notes'],
'notes': ['file','Notes'],
'highlights': ['paragraph', 'Found In Text'],
'trashed': ['poop', 'Trashed Notes'],
'tagged': ['tag', 'Tagged'],
},
noteSections: {
pinned: [],
archived: [],
shared:[],
sent:[],
notes: [],
highlights: [],
trashed: [],
tagged:[],
},
}
},
beforeMount(){
this.$parent.loginGateway()
//If user is on title view,
this.titleView = this.$store.getters.getIsUserOnMobile
this.$io.on('new_note_created', noteId => {
// Push new note to top of list and animate
this.updateSingleNote(noteId)
this.$store.dispatch('fetchAndUpdateUserTotals')
})
this.$io.on('note_attribute_modified', noteId => {
const drawFocus = !this.openNotes.includes(parseInt(noteId))
this.updateSingleNote(noteId, drawFocus)
//Do not update note if its open
if(this.openNotes.includes(parseInt(noteId))){
this.$store.dispatch('fetchAndUpdateUserTotals')
}
})
//Update title cards when new note text is saved
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
const drawFocus = !this.openNotes.includes(parseInt(noteId))
this.updateSingleNote(noteId, drawFocus)
})
this.$bus.$on('update_single_note', (noteId) => {
const drawFocus = !this.openNotes.includes(parseInt(noteId))
this.updateSingleNote(noteId, drawFocus)
})
//Update totals for app
this.$store.dispatch('fetchAndUpdateUserTotals')
//Close note event
this.$bus.$on('close_active_note', ({noteId, modified}) => {
this.closeNote(noteId, modified)
})
this.$bus.$on('note_deleted', (noteId) => {
//Remove deleted note from set, its deleted
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key].forEach( (note, index) => {
if(note.id == noteId){
this.noteSections[key].splice(index,1)
this.$store.dispatch('fetchAndUpdateUserTotals')
return
}
})
})
})
this.$bus.$on('update_fast_filters', filterIndex => {
this.updateFastFilters(filterIndex)
})
//Event to update search from other areas
this.$bus.$on('update_search_term', sentInSearchTerm => {
this.searchTerm = sentInSearchTerm
this.search(true, this.batchSize)
.then( () => {
this.searchAttachments()
const postData = {
'tagText':this.searchTerm.trim()
}
this.tagSuggestions = []
axios.post('/api/tag/suggest', postData)
.then( response => {
this.tagSuggestions = response.data
})
// return
})
})
//Reload page content - don't trigger if load is in progress
this.$bus.$on('note_reload', () => {
if(!this.showLoading){
this.reset()
}
})
// Window scroll needed when scrolling full page.
// second scroll event added on note-list for floating view scroll detection
window.addEventListener('scroll', this.onScroll)
//Close notes when back button is pressed
// window.addEventListener('hashchange', this.hashChangeAction)
//update note on visibility change
// document.addEventListener('visibilitychange', this.visibiltyChangeAction);
},
beforeDestroy(){
window.removeEventListener('scroll', this.onScroll)
// document.removeEventListener('visibilitychange', this.visibiltyChangeAction)
this.$bus.$off('note_reload')
this.$bus.$off('close_active_note')
// this.$bus.$off('update_single_note')
this.$bus.$off('note_deleted')
this.$bus.$off('update_fast_filters')
this.$bus.$off('update_search_term')
//We want to remove event listeners, but something here is messing them up and preventing ALL event listeners from working
// this.$off() // Remove all event listeners
// this.$bus.$off()
},
mounted() {
//Open note on PAGE LOAD if ID is set
if(this.$route.params.id > 1){
this.openNote(this.$route.params.id)
}
//Loads initial batch and tags
this.reset()
},
watch: {
'$route.params.id': function(id){
this.openNote(id)
},
'$route' (to, from) {
// Reload the notes if returning to this page
if(to.fullPath == '/notes' && !from.fullPath.includes('/notes/open/')){
this.reset()
}
// Close all notes if returning to /notes page
if(to.fullPath == '/notes' && from.fullPath.includes('/notes/open/')){
this.closeAllNotes()
}
//Lookup tags set in URL
if(to.params.tag && this.$store.getters.totals && this.$store.getters.totals['tags'][to.params.tag]){
//Lookup tag in store by string
const tagObject = this.$store.getters.totals['tags'][to.params.tag]
//Pull key out of string and load tags for that key
this.toggleTagFilter(tagObject.id)
return
}
}
},
computed: {
isFloatingList(){
//If note 1 or 2 is open, show floating column
return (this.openNotes.length > 0)
},
showOneColumn(){
return this.$store.getters.getIsUserOnMobile
}
},
methods: {
openNote(id, event = null){
//
const intId = parseInt(id)
if(this.openNotes.includes(intId)){
console.log('Open already open note?')
// const openIndex = this.openNotes.indexOf(intId)
// if(openIndex != -1){
// console.log('Open note and remove it ', intId + ' on index ' + openIndex)
// this.openNotes.splice(openIndex, 1)
// }
// this.$bus.$emit('close_note_by_id', intId)
return
}
//Don't open note if a link is clicked in display card
if(event && event.target && event.target.nodeName){
const nodeClick = event.target.nodeName
if(nodeClick == 'A'){ return }
}
// Push note to stack if not open
if(Number.isInteger(intId) && !this.openNotes.includes(intId)){
this.openNotes.push(intId)
}
this.$nextTick(() => {
// change route if open ID is not the same as current ID
if(this.$route.params.id != id){
console.log('Open note, change route -> route id ' + this.$route.params.id + ' note id ->' + id + ', ' +(this.$route.params.id == id))
this.$router.push('/notes/open/'+id)
}
})
return
},
closeNote(noteId, modified){
console.log('close note', this.$route.fullPath)
const openIndex = this.openNotes.indexOf(noteId)
if(openIndex != -1){
console.log('Removing note id ', noteId + ' on index ' + openIndex)
this.openNotes.splice(openIndex, 1)
}
// //A note has been closed
// if(this.$route.fullPath != '/notes'){
// this.$router.push('/notes')
// }
if(this.openNotes.length == 0 && this.$route.fullPath != '/notes'){
this.$router.push('/notes')
}
if(modified){
console.log('Just closed Note -> ' + noteId + ', modified -> ', modified)
this.$store.dispatch('fetchAndUpdateUserTotals')
//Focus and animate if modified
this.updateSingleNote(noteId, modified)
}
console.log('closeNote(): Open notes length ', this.openNotes.length)
},
closeAllNotes(){
console.log('Close all notes ------------')
for (let i = this.openNotes.length - 1; i >= 0; i--) {
console.log('Close all notes -> ' + this.openNotes[i])
this.closeNote(this.openNotes[i])
}
console.log('----------------')
},
toggleTagFilter(tagId){
this.searchTags = [tagId]
//Reset note set and load up notes and tags
if(this.searchTags.length > 0){
this.search(true, this.batchSize)
return
}
//If no tags are selected, reset entire page
this.reset()
},
onScroll(e){
if(!this.scrollLoadEnabled){
return
}
clearTimeout(this.loadingBatchTimeout)
this.loadingBatchTimeout = setTimeout(() => {
//Detect distance scrolled down the page
const scrolledDown = window.pageYOffset + window.innerHeight
//Get height of div to properly detect scroll distance down
const height = document.getElementById('app').scrollHeight
//Load if less than 500px from the bottom
if(((height - scrolledDown) < 500) && this.scrollLoadEnabled){
this.search(true, this.batchSize, true)
}
}, 50)
return
},
visibiltyChangeAction(event){
//Fuck this shit, just use web sockets
return
//@TODO - phase this out, update it via socket.io
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible'){
//Load initial batch, then tags, then other batch
this.search(false, this.firstLoadBatchSize)
.then( () => {
// return
})
}
this.lastVisibilityState = document.visibilityState
},
// @TODO Don't even trigger this if the note wasn't changed
updateSingleNote(noteId, focuseAndAnimate = true){
noteId = parseInt(noteId)
//Find local note, if it exists; continue
let note = null
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0] && this.$refs['note-'+noteId][0].note){
note = this.$refs['note-'+noteId][0].note
//Show that note is working on updating
this.$refs['note-'+noteId][0].showWorking = true
}
//Lookup one note using passed in ID
const postData = {
searchQuery: this.searchTerm,
searchTags: this.searchTags,
fastFilters:{
noteIdSet:[noteId]
}
}
//Note data must be fetched, then sorted into existing note data
axios.post('/api/note/search', postData)
.then(results => {
//Pull note data out of note set
let newNote = results.data.notes[0]
if(newNote === undefined){
return
}
// if old note data and new note data exists
if(note && newNote){
//go through each prop and update it with new values
Object.keys(newNote).forEach(prop => {
note[prop] = newNote[prop]
})
//Push new note to front if its modified or we want it to
if( note.updated != newNote.updated ){
// Find note, in section, move to front
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key].forEach( (searchNote, index) => {
if(searchNote.id == noteId){
//Remove note from location and push to front
this.noteSections[key].splice(index, 1)
this.noteSections[key].unshift(note)
return
}
})
})
}
if( focuseAndAnimate ){
this.$nextTick( () => {
//Trigger close animation on note
this.$refs['note-'+noteId][0].justClosed()
this.$refs['note-'+noteId][0].showWorking = false
})
}
}
//New notes don't exist in list, push them to the front
if(note == null){
this.noteSections.notes.unshift(newNote)
//Trigger close animation on note
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].justClosed()
this.$refs['note-'+noteId][0].showWorking = false
}
}
if(this.$refs['note-'+noteId] && this.$refs['note-'+noteId][0]){
this.$refs['note-'+noteId][0].showWorking = false
}
//Trigger section rebuild
this.rebuildNoteCategorise()
})
.catch(error => {
console.log(error)
this.$bus.$emit('notification', 'Failed to Update Note')
})
},
searchAttachments(){
axios.post('/api/attachment/textsearch', {'searchTerm':this.searchTerm})
.then(results => {
this.foundAttachments = results.data
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Attachments') })
},
search(showLoading = true, notesInNextLoad = 10, mergeExisting = false){
return new Promise((resolve, reject) => {
//Don't double load note batches
if(this.showLoading){
console.log('Loading already in progress')
return resolve(false)
}
if(!mergeExisting){
this.batchOffset = 0 // Reset batch offset if we are not merging note batches or new set will be offset from current and overwrite current set with second batch
}
//Remove all filter limits from previous queries
delete this.fastFilters.limitSize
delete this.fastFilters.limitOffset
let postData = {
searchQuery: this.searchTerm,
searchTags: this.searchTags,
fastFilters: this.fastFilters,
}
//Save initial post data on first load
if(this.initialPostData == null){
this.initialPostData = JSON.stringify(postData)
}
//If post data is not the same as initial, show clear button
if(JSON.stringify(postData) != this.initialPostData){
this.showClear = true
}
if(notesInNextLoad && notesInNextLoad > 0){
//Create limit based off of the number of notes already loaded
postData.fastFilters.limitSize = notesInNextLoad
postData.fastFilters.limitOffset = this.batchOffset
}
//Perform search - or die
this.showLoading = showLoading
this.scrollLoadEnabled = false
axios.post('/api/note/search', postData)
.then(response => {
//Reset a lot of stuff if we are not merging batches
if(!mergeExisting){
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key] = []
})
}
this.searchResultsCount = 0
// console.timeEnd('Fetch TitleCard Batch '+notesInNextLoad)
//Save the number of notes just loaded
this.batchOffset += response.data.notes.length
//Enable scroll loading if endpoint retured notes
this.scrollLoadEnabled = response.data.notes.length > 0
if(response.data.total > 0){
this.searchResultsCount = response.data.total
}
this.showLoading = false
this.generateNoteCategories(response.data.notes, mergeExisting)
//cache initial notes for faster reloads
if(!mergeExisting && this.showClear == false){
const cachedNotesJson = JSON.stringify(response.data.notes)
localStorage.setItem('snippetCache', cachedNotesJson)
}
return resolve(true)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Search Notes') })
})
},
rebuildNoteCategorise(){
let currentNotes = []
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key].forEach( note => {
currentNotes.push(note)
})
})
this.generateNoteCategories(currentNotes, false)
},
generateNoteCategories(notes, mergeExisting){
// Place each note in a category based on certain attributes and fast filters
//Reset all sections if we are not merging existing
if(!mergeExisting){
Object.keys(this.noteSections).forEach( key => {
this.noteSections[key] = []
})
}
//Sort notes into defined sections
notes.forEach(note => {
if(this.searchTerm.length > 0){
if(note.pinned == 1){
this.noteSections.pinned.push(note)
return
}
//Push to default note section
this.noteSections.notes.push(note)
return
}
//Display all tags in tag section
if(this.searchTags.length >= 1){
this.noteSections.tagged.push(note)
return
}
//Only show trashed notes when trashed
if(this.fastFilters.onlyShowTrashed == 1){
if(note.trashed == 1){
this.noteSections.trashed.push(note)
}
return
}
if(note.trashed == 1){
return
}
//Show archived notes
if(this.fastFilters.onlyArchived == 1){
if(note.pinned == 1 && note.archived == 1){
this.noteSections.pinned.push(note)
return
}
if(note.archived == 1){
this.noteSections.archived.push(note)
}
return
}
if(note.archived == 1){ return }
//Only show sent notes section if shared is selected
if(this.fastFilters.onlyShowSharedNotes == 1){
if(note.shared == 2){
this.noteSections.sent.push(note)
}
if(note.shareUsername != null){
this.noteSections.shared.push(note)
}
return
}
//Show shared notes on main list but not notes shared with you
if(note.shareUsername != null){ return }
// Pinned notes are always first, they can appear in the archive
if(note.pinned == 1){
this.noteSections.pinned.push(note)
return
}
//Push to default note section
this.noteSections.notes.push(note)
return
})
},
reset(){
this.showClear = false
this.scrollLoadEnabled = true
this.searchTerm = ''
this.searchTags = []
this.tagSuggestions = []
this.fastFilters = {}
this.foundAttachments = [] //Remove all attachments
this.updateFastFilters(5) //This loads notes
},
updateFastFilters(index){
//clear out tags
this.searchTags = []
this.tagSuggestions = []
this.showLoading = false
this.searchTerm = ''
this.$bus.$emit('reset_fast_filters') //Clear out search
const options = [
'withLinks', // 'Only Show Notes with Links'
'withTags', // 'Only Show Notes with Tags'
'onlyArchived', //'Only Show Archived Notes'
'onlyShowSharedNotes', //Only show shared notes
'onlyShowTrashed',
'notesHome',
]
let filter = {}
filter[options[index]] = 1
this.fastFilters = filter
//If notes exist in cache, load them up
let showLoading = true
const cachedNotesJson = localStorage.getItem('snippetCache')
const cachedNotes = JSON.parse(cachedNotesJson)
if(cachedNotes && cachedNotes.length > 0 && !this.showClear){
//Load cache. do not merge existing
this.generateNoteCategories(cachedNotes, false)
showLoading = false
}
//Fetch First batch of notes with new filter
this.search(showLoading, this.batchSize, false)
// .then( r => this.search(false, this.batchSize, true))
}
}
}
</script>
<style type="text/css" scoped>
.text-fix {
padding: 8px 0 0 15px;
display: inline-block;
color: var(--menu-accent);
}
.detail {
float: right;
}
.note-card-display-area {
display: flex;
flex-wrap: wrap;
}
.display-area-title {
width: 100%;
display: inline-block;
}
.note-card-section {
/*padding-bottom: 15px;*/
}
.note-card-section + .note-card-section {
padding: 15px 0 0;
}
.loading-section {
color: var(--main-accent);
box-shadow: 0 1px 3px 0 var(--main-accent);
border-radius: 6px;
background-color: var(--small_element_bg_color);
display: inline-block;
width: 100%;
margin: 15px 0;
}
.floating-list {
z-index: 1000;
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 25%;
height: 100vh;
background-color: var(--small_element_bg_color);
padding: 15px 5px 0px 10px;
overflow-y: scroll;
overflow-x: hidden;
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
background-color: var(--border_color);
}
.floating-list::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
.note-panel-container {
position: fixed;
width: 75%;
height: 100vh;
background: gray;
top: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
align-items: stretch;
align-content: stretch;
z-index: 1000;
}
.note-panel-fullwidth {
width: 100% !important;
}
.note-panel-container > div {
flex: 1;
position: relative;
}
.hidden-floating-list {
left: -1000px !important;
}
.show-hidden-note-list-button {
position: fixed;
top: 25px;
left: 0;
min-width: 45px;
background-color: var(--main-accent);
color: var(--text_color);
display: block;
z-index: 1100;
cursor: pointer;
border-bottom-right-radius: 5px;
border-top-right-radius: 5px;
padding: 8px 0px 8px 13px;
text-align: left;
font-size: 1.4em;
}
@media (min-width:320px) { /* smartphones, iPhone, portrait 480x320 phones */
.floating-list {
left: -1000px;
}
.note-panel-container {
width: 100%;
}
}
@media (min-width:481px) { /* portrait e-readers (Nook/Kindle), smaller tablets @ 600 or @ 640 wide. */
.floating-list {
left: 0px;
}
.note-panel-container {
width: 75%;
}
}
@media (min-width:641px) { /* portrait tablets, portrait iPad, landscape e-readers, landscape 800x480 or 854x480 phones */
}
@media (min-width:961px) { /* tablet, landscape iPad, lo-res laptops ands desktops */
}
@media (min-width:1025px) { /* big landscape tablets, laptops, and desktops */
}
@media (min-width:1281px) { /* hi-res laptops and desktops */
}
@media (min-width:2000px) { /* BIG hi-res laptops and desktops */
.floating-list {
left: 180px;
width: calc(30% - 180px);
}
.note-panel-container {
width: 70%;
}
}
.master-note-edit {
position: absolute;
width: 100%;
background: var(--small_element_bg_color);
left: 0;
top: 0;
bottom: 0;
overflow: hidden;
}
.master-note-edit + .master-note-edit {
border-left: 2px solid var(--main-accent);
border-left: 5px solid var(--border_color);
}
/*html, body {
height: 100%;
}
.wrap {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}*/
</style>