Recursive tree version

This commit is contained in:
Magnus Åhall 2026-02-24 17:21:55 +01:00
parent 42c5d43610
commit 97058d036d
8 changed files with 599 additions and 134 deletions

View file

@ -1,16 +1,85 @@
export class Application {
constructor() {
this.addTopHandlers()
}
constructor(records) {// {{{
this.records = this.parseRecords(records)
this.folders = this.createFolders()
this.render()
}// }}}
parseRecords(recordsData) {// {{{
const records = recordsData.map(d => new Record(d))
return records
}// }}}
createFolders() {// {{{
this.records.sort(this.sortRecords)
const topFolder = new Folder(this, 'root')
addTopHandlers() {
for (const top of document.querySelectorAll('.top'))
top.addEventListener('click', event => this.handlerTop(event))
}
for (const rec of this.records) {
const labels = rec.labels().reverse().slice(0, -1)
handlerTop(event) {
let currFolder = topFolder
let accFolderLabels = []
for (const i in labels) {
const label = labels[i]
accFolderLabels.push(label)
const accFolderName = accFolderLabels.map(v => v).reverse().join('.')
let folder = currFolder.subfolders.get(label)
if (folder === undefined) {
folder = new Folder(this, accFolderName)
currFolder.subfolders.set(label, folder)
}
currFolder = folder
// Add the record to the innermost folder
}
currFolder.addRecord(rec)
}
return topFolder
}// }}}
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() {// {{{
const tree = document.getElementById('records-tree')
tree.replaceChildren()
// Top root folder doesn't have to be shown.
const folders = Array.from(this.folders.subfolders.values())
folders.sort(this.sortFolders)
for (const folder of folders)
tree.append(folder.toElement())
}// }}}
handlerTop(event) {// {{{
const topEl = event.target.closest('.top')
console.log(topEl.dataset.self)
let records, types, values
if (topEl.classList.contains('open')) {
@ -30,12 +99,22 @@ export class Application {
topEl.classList.remove('open')
} else {
records = document.querySelectorAll(`[data-top="${topEl.dataset.self}"]`)
types = document.querySelectorAll(`[data-top="${topEl.dataset.self}"] + .type`)
values = document.querySelectorAll(`[data-top="${topEl.dataset.self}"] + .type + .value`)
if (event.shiftKey) {
records = document.querySelectorAll(`[data-top$="${topEl.dataset.self}"]`)
types = document.querySelectorAll(`[data-top$="${topEl.dataset.self}"] + .type`)
values = document.querySelectorAll(`[data-top$="${topEl.dataset.self}"] + .type + .value`)
} else {
records = document.querySelectorAll(`[data-top="${topEl.dataset.self}"]`)
types = document.querySelectorAll(`[data-top="${topEl.dataset.self}"] + .type`)
values = document.querySelectorAll(`[data-top="${topEl.dataset.self}"] + .type + .value`)
console.log(records)
}
for (const r of records)
for (const r of records) {
r.classList.add('show')
if (event.shiftKey && r.classList.contains('top'))
r.classList.add('open')
}
for (const r of types)
r.classList.add('show')
for (const r of values)
@ -45,5 +124,255 @@ export class Application {
}
}// }}}
}
class Folder {
constructor(app, name) {// {{{
this.application = app
this.open = false
this.folderName = name
this.subfolders = new Map()
this.records = []
this.div = null
this.divSubfolders = null
this.divRecords = null
}// }}}
name() {// {{{
return this.folderName.toLowerCase()
}// }}}
labels() {// {{{
return this.name().split('.')
}// }}}
addRecord(rec) {// {{{
this.records.push(rec)
}// }}}
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)
}// }}}
toElement() {// {{{
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')
this.div.dataset.top = this.labels().slice(1).join('.')
this.div.dataset.self = this.name()
const firstLabel = this.labels()[0]
const restLabels = this.labels().slice(1).join('.')
this.div.innerHTML = `
<div class="label">
<img class="closed" src="/images/${_VERSION}/icon_folder.svg">
<img class="open" src="/images/${_VERSION}/icon_folder_open.svg">
<span>${firstLabel}</span><span>${restLabels != '' ? '.' + restLabels : ''}</span>
</div>
<div class="subfolders"></div>
<div class="records"></div>
`
this.divSubfolders = this.div.querySelector('.subfolders')
this.divRecords = this.div.querySelector('.records')
this.div.querySelector('.label').addEventListener('click', event => this.toggleFolder(event))
}
// Subfolders are refreshed.
this.divSubfolders.replaceChildren()
const subfolders = Array.from(this.subfolders.values())
subfolders.sort(this.application.sortFolders)
for (const folder of subfolders)
this.divSubfolders.append(folder.toElement())
// Records are refreshed.
this.divRecords.replaceChildren()
for (const rec of Array.from(this.records))
this.divRecords.append(...rec.toElements())
return this.div
}// }}}
}
class Record {
constructor(data) {// {{{
this.data = data
this.imgIcon = null
this.divFQDN = null
this.divType = null
this.divValue = null
this.divSeparator = null
}// }}}
id() {// {{{
return this.data['.id']
}// }}}
disabled() {// {{{
return this.data.Disabled === 'true'
}// }}}
dynamic() {// {{{
return this.data.Dynamic === 'true'
}// }}}
name() {// {{{
return this.data.Name.toLowerCase()
}// }}}
ttl() {// {{{
return this.data.TTL
}// }}}
type() {// {{{
return this.data.Type.toUpperCase()
}// }}}
value() {// {{{
switch (this.type()) {
case 'A':
case 'AAAA':
return this.data.Address
case 'CNAME':
return this.data.CNAME
}
}// }}}
labels() {// {{{
return this.name().split('.')
}// }}}
copy(el, text) {// {{{
el.classList.add('copy')
navigator.clipboard.writeText(text)
setTimeout(() => el.classList.remove('copy'), 200)
}// }}}
edit() {// {{{
new RecordDialog(this).show()
}// }}}
set(key, value) {
if (key != 'Value') {
this.data[key] = value
return
}
switch(this.data.type.toUpperCase()) {
case 'A':
case 'AAAA':
this.data.Address = value
break
case 'CNAME':
this.data.CNAME = value
break
}
}
toElements() {// {{{
if (this.divFQDN === null) {
this.imgIcon = document.createElement('img')
this.divFQDN = document.createElement('div')
this.divType = document.createElement('div')
this.divValue = document.createElement('div')
this.divSeparator = document.createElement('div')
this.imgIcon.src = `/images/${_VERSION}/icon_record.svg`
this.divFQDN.classList.add('fqdn')
this.divType.classList.add('type')
this.divValue.classList.add('value')
this.divSeparator.classList.add('separator')
this.divFQDN.innerHTML = `
<span class="first-label">${this.labels()[0]}</span>
<span class="rest-label">${this.labels().slice(1).join('.')}</span>
`
this.divFQDN.addEventListener('click', event => {
if (event.shiftKey)
this.copy(event.target.closest('.fqdn'), this.name())
else
this.edit()
})
this.divValue.addEventListener('click', event => {
if (event.shiftKey)
this.copy(event.target.closest('.value'), this.value())
else
this.edit()
})
}
this.divType.innerText = this.type()
this.divValue.innerText = this.value()
return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divSeparator]
}// }}}
}
class RecordDialog {
constructor(record) {
this.record = record
}
show() {
const dlg = document.createElement('dialog')
dlg.id = "record-dialog"
dlg.innerHTML = `
<div>Name</div>
<input type="text" class="name">
<div>Type</div>
<select class="type">
<option>A</option>
<option>AAAA</option>
<option>CNAME</option>
</select>
<div>Value</div>
<input type="text" class="value">
<div>TTL</div>
<input type="text" class="ttl">
<div class="buttons">
<button class="save">Save</button>
<button class="close">Close</button>
</div>
`
dlg.querySelector('.name').value = this.record.name()
dlg.querySelector('.type').value = this.record.type()
dlg.querySelector('.value').value = this.record.value()
dlg.querySelector('.ttl').value = this.record.ttl()
dlg.querySelector('.save').addEventListener('click', ()=>this.save())
dlg.querySelector('.close').addEventListener('click', ()=>dlg.close())
dlg.addEventListener('close', ()=>dlg.remove())
document.body.appendChild(dlg)
dlg.showModal()
}
save() {
this.record.set('Name', dlg.querySelector('.name').value)
this.record.set('Type', dlg.querySelector('.type').value)
this.record.set('Value', dlg.querySelector('.value').value)
this.record.set('TTL', dlg.querySelector('.ttl').value)
}
}