Recursive tree version
This commit is contained in:
parent
42c5d43610
commit
97058d036d
8 changed files with 599 additions and 134 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue