export class Application { 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') for (const rec of this.records) { const labels = rec.labels().reverse().slice(0, -1) 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') let records, types, values if (topEl.classList.contains('open')) { 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`) for (const r of records) { r.classList.remove('show') r.classList.remove('open') } for (const r of types) r.classList.remove('show') for (const r of values) r.classList.remove('show') topEl.classList.remove('open') } else { 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) { 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) r.classList.add('show') topEl.classList.add('open') } }// }}} } 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 = `
${firstLabel}${restLabels != '' ? '.' + restLabels : ''}
` 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.render()) 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() {// {{{ return this.data.ParsedValue }// }}} 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) {// {{{ this.data[key] = value }// }}} render() {// {{{ 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 = ` ${this.labels()[0]} ${this.labels().slice(1).join('.')} ` 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] }// }}} save() {// {{{ fetch('/record/save', { method: 'POST', body: JSON.stringify(this.data), }) .then(data => data.json()) .then(json => { if (!json.OK) { alert(json.Error) return } }) }// }}} } class RecordDialog { constructor(record) {// {{{ this.record = record }// }}} show() {// {{{ this.dlg = document.createElement('dialog') this.dlg.id = "record-dialog" this.dlg.innerHTML = `
Name
Type
Value
TTL
` this.dlg.querySelector('.name').value = this.record.name() this.dlg.querySelector('.type').value = this.record.type() this.dlg.querySelector('.value').value = this.record.value() this.dlg.querySelector('.ttl').value = this.record.ttl(); ['.name', '.type', '.value', '.ttl'].forEach(v => this.dlg.querySelector(v).addEventListener('keydown', event => this.enterKeyHandler(event)) ) this.dlg.querySelector('.save').addEventListener('click', () => this.save()) this.dlg.querySelector('.close').addEventListener('click', () => this.dlg.close()) this.dlg.addEventListener('close', () => this.dlg.remove()) document.body.appendChild(this.dlg) this.dlg.showModal() }// }}} enterKeyHandler(event) {// {{{ if (event.key == "Enter") this.save() }// }}} save() {// {{{ this.record.set('Name', this.dlg.querySelector('.name').value) this.record.set('Type', this.dlg.querySelector('.type').value) this.record.set('ParsedValue', this.dlg.querySelector('.value').value) this.record.set('TTL', this.dlg.querySelector('.ttl').value) this.record.render() this.record.save() this.dlg.close() }// }}} }