Refactor debug Javascript code (for DRY-ness, ...)
authorBenjamin Braatz <bb@bbraatz.eu>
Sun, 7 Mar 2021 19:21:21 +0000 (20:21 +0100)
committerBenjamin Braatz <bb@bbraatz.eu>
Sun, 7 Mar 2021 20:00:57 +0000 (21:00 +0100)
web/controlpi-debug.js

index e9e39dd4bc9ff1d321e0e91f3a8e13fc8290ca69..8c6eebcbccff34f37ae979e4a7b88cca101d7657 100644 (file)
@@ -1,11 +1,3 @@
-// Open Websocket back to ControlPi we were loaded from:
-const websocket = new WebSocket("ws://" + window.location.host)
-
-// When Websocket is ready request all clients from bus:
-websocket.addEventListener('open', function (event) {
-    websocket.send(JSON.stringify({target: '', command: 'get clients'}))
-})
-
 // Create section for client:
 function createForClient(client) {
     const section = document.createElement('section')
@@ -46,9 +38,19 @@ function createForMessage(message, isMessage) {
     } else {
         const table = document.createElement('table')
         for (const key in message) {
-            if (isMessage && key == 'sender') {
-                // Ignore 'sender' for messages (redundant in client heading):
-                continue
+            if (isMessage) {
+                if (key == 'sender') {
+                    // Ignore 'sender' for last received messages
+                    // (information redundantly present in client heading):
+                    continue
+                } else if (key == 'state') {
+                    // Set background according to state:
+                    if (message[key]) {
+                        div.classList.add('green')
+                    } else {
+                        div.classList.add('red')
+                    }
+                }
             }
             // Append table row for key-value pair:
             const tr = document.createElement('tr')
@@ -75,9 +77,15 @@ function createForMessage(message, isMessage) {
 // Resize input fields to current input:
 function resizeInput() {
     const tmp = document.createElement('span')
-    // Hack: Should get this from CSS class rather than hardcode it:
+    // HACK: Should get this from CSS rather than hardcode it:
     tmp.setAttribute('style', 'font-size: 0.8rem;')
-    const tmpContent = document.createTextNode(this.value)
+    var value = this.value
+    // HACK: 'number' fields for floats do not return trailing dot in
+    // value, so we add one unconditionally:
+    if (this.type == 'number' && this.step == 'any' && !value.includes('.')) {
+        value += '.'
+    }
+    const tmpContent = document.createTextNode(value)
     tmp.appendChild(tmpContent)
     document.body.appendChild(tmp);
     const width = tmp.getBoundingClientRect().width
@@ -85,6 +93,97 @@ function resizeInput() {
     this.style.width = width + 'px'
 }
 
+// Create form input (or select) for key-value pairs in receive templates:
+function inputsForKeyValue(key, value) {
+    result = []
+    literal = true
+    if (value.match(/^<class '(str|int|float|bool)'>$/g)) {
+        literal = false
+    }
+    if (value == '<class \'bool\'>' || typeof(value) == 'boolean') {
+        // Create select with true and false options for Boolean:
+        const select = document.createElement('select')
+        select.setAttribute('name', key)
+        const optionTrue = document.createElement('option')
+        optionTrue.setAttribute('value', 'true')
+        const optionTrueContent = document.createTextNode('true')
+        optionTrue.appendChild(optionTrueContent)
+        select.appendChild(optionTrue)
+        const optionFalse = document.createElement('option')
+        optionFalse.setAttribute('value', false)
+        const optionFalseContent = document.createTextNode('false')
+        optionFalse.appendChild(optionFalseContent)
+        if (literal) {
+            // Select set value and disable other value
+            // for literal Boolean:
+            if (value) {
+                optionTrue.setAttribute('selected', '')
+                optioniFalse.setAttribute('disabled', '')
+            } else {
+                optionFalse.setAttribute('selected', '')
+                optionTrue.setAttribute('disabled', '')
+            }
+        }
+        select.appendChild(optionFalse)
+        result.push(select)
+    } else {
+        // Create input for everything except Booleans:
+        if (value == '<class \'str\'>' ||
+            (literal && typeof(value) == 'string' && key != 'command')) {
+            // Quote strings:
+            const openquote = document.createTextNode('"')
+            result.push(openquote)
+        }
+        const input = document.createElement('input')
+        // Set type of input:
+        if (key == 'command') {
+            input.setAttribute('type', 'hidden')
+        } else if (value == '<class \'int\'>' || value == '<class \'float\'>' ||
+                   typeof(value) == 'number') {
+            input.setAttribute('type', 'number')
+            if (value == '<class \'int\'>') {
+                input.setAttribute('step', '1')
+            } else if (value == '<class \'float\'>') {
+                input.setAttribute('step', 'any')
+            }
+        } else if (value == '<class \'str\'>' || typeof(value) == 'string') {
+            input.setAttribute('type', 'text')
+        }
+        // Set key as name of input:
+        input.setAttribute('name', key)
+        // Set value of input, readonly or required:
+        if (key == 'command') {
+            input.setAttribute('value', value)
+        } else if (literal) {
+            input.setAttribute('value', value)
+            input.setAttribute('readonly', '')
+        } else {
+            input.setAttribute('value', '')
+            input.setAttribute('required', '')
+        }
+        if (key != 'command') {
+            // Resize input field at every change and at beginning:
+            input.addEventListener('input', resizeInput)
+            resizeInput.call(input)
+        }
+        result.push(input)
+        if (value == '<class \'str\'>' ||
+            (literal && typeof(value) == 'string' && key != 'command')) {
+            // Quote strings:
+            const closequote = document.createTextNode('"')
+            result.push(closequote)
+        }
+        if (key == 'command') {
+            // Add submit button for 'command' key:
+            const submit = document.createElement('input')
+            submit.setAttribute('type', 'submit')
+            submit.setAttribute('value', value)
+            result.push(submit)
+        }
+    }
+    return result
+}
+
 // Create div, form and table for command template:
 function createForCommand(template) {
     const div = document.createElement('div')
@@ -121,113 +220,8 @@ function createForCommand(template) {
         keyTd.appendChild(keyTdContent)
         tr.appendChild(keyTd)
         const valueTd = document.createElement('td')
-        value = template[key]
-        // Create submit button for command itself:
-        if (key == 'command') {
-            const hidden = document.createElement('input')
-            hidden.setAttribute('type', 'hidden')
-            hidden.setAttribute('name', key)
-            hidden.setAttribute('value', value)
-            valueTd.appendChild(hidden)
-            const submit = document.createElement('input')
-            submit.setAttribute('type', 'submit')
-            submit.setAttribute('value', value)
-            valueTd.appendChild(submit)
-        // Create appropriate input fields for '<class ...>':
-        } else if (value == '<class \'str\'>') {
-            const openquote = document.createTextNode('"')
-            valueTd.appendChild(openquote)
-            const input = document.createElement('input')
-            input.setAttribute('type', 'text')
-            input.setAttribute('name', key)
-            input.setAttribute('value', '')
-            input.setAttribute('required', '')
-            input.addEventListener('input', resizeInput)
-            resizeInput.call(input)
-            valueTd.appendChild(input)
-            const closequote = document.createTextNode('"')
-            valueTd.appendChild(closequote)
-        } else if (value == '<class \'int\'>') {
-            const input = document.createElement('input')
-            input.setAttribute('type', 'number')
-            input.setAttribute('step', '1')
-            input.setAttribute('name', key)
-            input.setAttribute('value', '')
-            input.setAttribute('required', '')
-            input.addEventListener('input', resizeInput)
-            resizeInput.call(input)
-            valueTd.appendChild(input)
-        } else if (value == '<class \'float\'>') {
-            const input = document.createElement('input')
-            input.setAttribute('type', 'number')
-            input.setAttribute('step', 'any')
-            input.setAttribute('name', key)
-            input.setAttribute('value', '')
-            input.setAttribute('required', '')
-            input.addEventListener('input', resizeInput)
-            resizeInput.call(input)
-            valueTd.appendChild(input)
-        } else if (value == '<class \'bool\'>') {
-            const select = document.createElement('select')
-            select.setAttribute('name', key)
-            const optionTrue = document.createElement('option')
-            optionTrue.setAttribute('value', true)
-            const optionTrueContent = document.createTextNode('true')
-            optionTrue.appendChild(optionTrueContent)
-            select.appendChild(optionTrue)
-            const optionFalse = document.createElement('option')
-            optionFalse.setAttribute('value', false)
-            const optionFalseContent = document.createTextNode('false')
-            optionFalse.appendChild(optionFalseContent)
-            select.appendChild(optionFalse)
-            valueTd.appendChild(select)
-        // Create readonly input fields for literals:
-        } else if (typeof(value) == 'string') {
-            const openquote = document.createTextNode('"')
-            valueTd.appendChild(openquote)
-            const input = document.createElement('input')
-            input.setAttribute('type', 'text')
-            input.setAttribute('name', key)
-            input.setAttribute('value', value)
-            input.setAttribute('readonly', '')
-            input.addEventListener('input', resizeInput)
-            resizeInput.call(input)
-            valueTd.appendChild(input)
-            const closequote = document.createTextNode('"')
-            valueTd.appendChild(closequote)
-        } else if (typeof(value) == 'number') {
-            const input = document.createElement('input')
-            input.setAttribute('type', 'number')
-            input.setAttribute('name', key)
-            input.setAttribute('value', value)
-            input.setAttribute('readonly', '')
-            input.addEventListener('input', resizeInput)
-            resizeInput.call(input)
-            valueTd.appendChild(input)
-        } else if (typeof(value) == 'boolean') {
-            const select = document.createElement('select')
-            select.setAttribute('name', key)
-            const optionTrue = document.createElement('option')
-            optionTrue.setAttribute('value', 'true')
-            if (value) {
-                optionTrue.setAttribute('selected', '')
-            } else {
-                optionTrue.setAttribute('disabled', '')
-            }
-            const optionTrueContent = document.createTextNode('true')
-            optionTrue.appendChild(optionTrueContent)
-            select.appendChild(optionTrue)
-            const optionFalse = document.createElement('option')
-            optionFalse.setAttribute('value', 'false')
-            if (value) {
-                optionTrue.setAttribute('disabled', '')
-            } else {
-                optionTrue.setAttribute('selected', '')
-            }
-            const optionFalseContent = document.createTextNode('false')
-            optionFalse.appendChild(optionFalseContent)
-            select.appendChild(optionFalse)
-            valueTd.appendChild(select)
+        for (const element of inputsForKeyValue(key, template[key])) {
+            valueTd.appendChild(element)
         }
         tr.appendChild(valueTd)
         table.appendChild(tr)
@@ -237,66 +231,72 @@ function createForCommand(template) {
     return div
 }
 
-// When receiving message from ControlPi through Websocket:
-websocket.addEventListener('message', function (event) {
-    const message = JSON.parse(event.data)
-    if (message.sender == '') {
-        // When message is from bus:
-        if (message.event == 'unregistered') {
-            // On deregistration delete section if it exists:
-            const clientElement = document.getElementById(message.client)
-            if (clientElement != null) {
-                clientElement.remove()
-            }
-        } else {
-            // On registration or initial answers for all clients:
-            const clientElement = document.getElementById(message.client)
-            if (clientElement == null) {
-                // Create section for client if not existent:
-                const main = document.getElementById('ControlPi Debug')
-                main.appendChild(createForClient(message.client))
-            }
-            const receiveContainer = document.getElementById(message.client + ' Receives')
-            receiveContainer.innerHTML = '<h3>=&gt;</h3>'
-            for (const template of message.receives) {
-                if (template.command != null) {
-                    const templateElement = createForCommand(template)
-                    receiveContainer.appendChild(templateElement)
-                } else {
-                    const templateElement = createForMessage(template)
-                    receiveContainer.appendChild(templateElement)
-                }
-            }
-            const sendContainer = document.getElementById(message.client + ' Sends')
-            sendContainer.innerHTML = '<h3>&lt;=</h3>'
-            for (const template of message.sends) {
-                const templateElement = createForMessage(template)
-                sendContainer.appendChild(templateElement)
-            }
+// Remove (if unregistered) or create (if not existent) elements for
+// clients and update interface information:
+function processBusMessage(message) {
+    // When message is from bus:
+    if (message['event'] == 'unregistered') {
+        // On deregistration delete client element if it exists:
+        const clientElement = document.getElementById(message['client'])
+        if (clientElement != null) {
+            clientElement.remove()
         }
     } else {
-        // When message is from client:
-        const clientElement = document.getElementById(message.sender)
+        // On registration or 'get clients' answer:
+        const clientElement = document.getElementById(message['client'])
         if (clientElement == null) {
-            // Create section for client if not existent:
+            // Create element for client if not existent:
             const main = document.getElementById('ControlPi Debug')
-            main.appendChild(createForClient(message.sender))
+            main.appendChild(createForClient(message['client']))
         }
-        // Update last received message:
-        const lastContainer = document.getElementById(message.sender + ' Last')
-        lastContainer.innerHTML = ''
-        const messageElement = createForMessage(message, true)
-        lastContainer.appendChild(messageElement)
-        if (message.state != null) {
-            // If this is some kind of state set background
-            // green or red depending on it:
-            if (message.state) {
-                messageElement.classList.remove('red')
-                messageElement.classList.add('green')
+        // Crate message elements for receives interface:
+        const receiveContainer = document.getElementById(message['client'] + ' Receives')
+        receiveContainer.innerHTML = '<h3>=&gt;</h3>'
+        for (const template of message.receives) {
+            if (template['command'] != null) {
+                receiveContainer.appendChild(createForCommand(template))
             } else {
-                messageElement.classList.remove('green')
-                messageElement.classList.add('red')
+                receiveContainer.appendChild(createForMessage(template))
             }
         }
+        // Create message elements for sends interface:
+        const sendContainer = document.getElementById(message['client'] + ' Sends')
+        sendContainer.innerHTML = '<h3>&lt;=</h3>'
+        for (const template of message.sends) {
+            sendContainer.appendChild(createForMessage(template))
+        }
+    }
+}
+
+// Create element for client (if not existent)
+// and update last received message:
+function processClientMessage(message) {
+    const clientElement = document.getElementById(message['sender'])
+    if (clientElement == null) {
+        // Create section for client if not existent:
+        const main = document.getElementById('ControlPi Debug')
+        main.appendChild(createForClient(message.sender))
+    }
+    // Update last received message:
+    const lastContainer = document.getElementById(message['sender'] + ' Last')
+    lastContainer.innerHTML = ''
+    lastContainer.appendChild(createForMessage(message, true))
+}
+
+// Open Websocket back to ControlPi we were loaded from:
+const websocket = new WebSocket("ws://" + window.location.host)
+
+// When Websocket is ready request all clients from bus:
+websocket.addEventListener('open', function (event) {
+    websocket.send(JSON.stringify({target: '', command: 'get clients'}))
+})
+
+// When receiving message from ControlPi through Websocket:
+websocket.addEventListener('message', function (event) {
+    const message = JSON.parse(event.data)
+    if (message['sender'] == '') {
+        processBusMessage(message)
+    } else {
+        processClientMessage(message)
     }
 })