import { MessageBus } from '@mbus'
export class Application {
constructor(records) {// {{{
window._mbus = new MessageBus()
this.settings = new Settings()
this.records = this.parseRecords(records)
this.topFolder = new Folder(this, null, 'root')
this.recordsTree = null
this.settingsIcon = null
this.renderFolders()
this.render()
}// }}}
parseRecords(recordsData) {// {{{
const records = recordsData.map(d => new Record(d))
return records
}// }}}
// cleanFolders removes all records from all folders.
// renderFolders can then put moved records in the correct
// (or newly created) folders again when record names are updated.
cleanFolders(folder) {// {{{
if (folder === undefined)
folder = this.topFolder
folder.records = []
folder.subfolders.forEach((folder, _label) => {
this.cleanFolders(folder)
})
}// }}}
deleteRecord(id) {// {{{
const i = this.records.findIndex(rec => rec.id() == id)
this.records.splice(i, 1)
}// }}}
renderFolders() {// {{{
this.records.sort(this.sortRecords)
// rec: for example www.google.com
for (const rec of this.records) {
// com.google (reverse and remove wwww)
const labels = rec.labels().reverse().slice(0, -1)
// Start each record from the top and iterate through all its labels
// except the first one since that would be the actual record.
let currFolder = this.topFolder
let accFolderLabels = []
for (const i in labels) {
const label = labels[i]
// The accumulated name is used to create each folder as the record progresses.
accFolderLabels.push(label)
const accFolderName = accFolderLabels.map(v => v).reverse().join('.')
// A new folder is created only when it doesn't exist
// to be able to update them when necessary.
let folder = currFolder.subfolders.get(label)
if (folder === undefined) {
folder = new Folder(this, currFolder, accFolderName)
currFolder.subfolders.set(label, folder)
}
currFolder = folder
}
// Add the record to the innermost folder
currFolder.addRecord(rec)
}
}// }}}
sortFolders(a, b) {// {{{
const aLabels = a.labels().reverse()
const bLabels = b.labels().reverse()
for (let i = 0; i < aLabels.length && i < bLabels.length; i++) {
if (aLabels[i] < bLabels[i])
return -1
if (aLabels[i] > bLabels[i])
return 1
}
if (a.length < b.length) return 1
if (a.length > b.length) return -1
return 0
}// }}}
sortRecords(a, b) {// {{{
const aLabels = a.labels().reverse()
const bLabels = b.labels().reverse()
for (let i = 0; i < aLabels.length && i < bLabels.length; i++) {
if (aLabels[i] < bLabels[i])
return -1
if (aLabels[i] > bLabels[i])
return 1
}
return 0
}// }}}
render() {// {{{
if (this.recordsTree == null) {
this.recordsTree = document.createElement('div')
this.recordsTree.id = 'records-tree'
this.createIcon = document.createElement('img')
this.createIcon.id = 'create-icon'
this.createIcon.src = `/images/${_VERSION}/icon_create.svg`
this.createIcon.addEventListener('click', () => new RecordDialog(new Record()).show())
this.settingsIcon = document.createElement('img')
this.settingsIcon.id = 'settings-icon'
this.settingsIcon.src = `/images/${_VERSION}/icon_settings.svg`
this.settingsIcon.addEventListener('click', () => new SettingsDialog(this).show())
document.body.appendChild(this.recordsTree)
document.body.appendChild(this.settingsIcon)
document.body.appendChild(this.createIcon)
document.body.addEventListener('keydown', event => this.handlerKeys(event))
}
// Top root folder doesn't have to be shown.
const folders = Array.from(this.topFolder.subfolders.values())
folders.sort(this.sortFolders)
for (const folder of folders)
this.recordsTree.append(folder.render())
// Subscribe to settings update since the elements they will change
// exists now.
_mbus.subscribe('settings_updated', event => this.handlerSettingsUpdated(event.detail))
this.setBoxedFolders(this.settings.get('boxed_folders'))
}// }}}
handlerKeys(event) {// {{{
let handled = true
// Every keyboard shortcut for the application wide handler is using Alt+Shift
// for consistency and that it works with a lot of browsers.
if (!event.altKey || !event.shiftKey || event.ctrlKey) {
return
}
switch (event.key.toLowerCase()) {
case 'n':
const existingDialog = document.getElementById('record-dialog')
if (existingDialog === null)
new RecordDialog(new Record()).show()
break
default:
handled = false
}
if (handled) {
event.stopPropagation()
event.preventDefault()
}
}// }}}
handlerSettingsUpdated({ key, value }) {// {{{
if (key == 'boxed_folders') {
this.setBoxedFolders(value)
}
}// }}}
setBoxedFolders(state) {// {{{
if (state)
document.body.classList.add('boxed-folders')
else
document.body.classList.remove('boxed-folders')
}// }}}
}
class Folder {
constructor(app, parentFolder, name) {// {{{
this.application = app
this.parentFolder = parentFolder
this.folderName = name
this.subfolders = new Map()
this.records = []
this.div = null
this.divSubfolders = null
this.divRecords = null
const topLevelOpen = this.application.settings.get('toplevel_open')
this.open = (topLevelOpen && this.labels().length <= 1)
}// }}}
name() {// {{{
return this.folderName.toLowerCase()
}// }}}
labels() {// {{{
return this.name().split('.')
}// }}}
addRecord(rec) {// {{{
this.records.push(rec)
rec.setFolder(this)
}// }}}
openFolder(recursive) {// {{{
this.open = true
this.div?.classList.add('open')
this.div?.classList.remove('closed')
if (recursive)
this.subfolders.forEach(folder => folder.openFolder(recursive))
}// }}}
closeFolder(recursive) {// {{{
this.open = false
this.div.classList.remove('open')
this.div.classList.add('closed')
if (recursive)
this.subfolders.forEach(folder => folder.closeFolder(recursive))
}// }}}
toggleFolder(event) {// {{{
event.stopPropagation()
if (this.open)
this.closeFolder(event.shiftKey)
else
this.openFolder(event.shiftKey)
}// }}}
render() {// {{{
if (this.div === null) {
this.div = document.createElement('div')
this.div.classList.add('folder')
this.div.classList.add(this.open ? 'open' : 'closed')
if (this.labels().length == 1)
this.div.classList.add('top-most')
const firstLabel = this.labels()[0]
const restLabels = this.labels().slice(1).join('.')
this.div.innerHTML = `