Skip to content

Commit

Permalink
uib-sidebar alpha 3 - input messages now work. The HTML editor now wo…
Browse files Browse the repository at this point in the history
…rks better. Inputs on the sidebar UI automatically send data back to the node's output.
  • Loading branch information
TotallyInformation committed Jan 18, 2025
1 parent 3848918 commit 7a1a261
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 87 deletions.
28 changes: 27 additions & 1 deletion docs/roadmap/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,36 @@ title: Possible Future Features
description: |
What is being worked on for the next release.
created: 2025-01-05 12:34:47
updated: 2025-01-08 17:58:24
updated: 2025-01-18 17:51:31
author: Julian Knight (Totally Information)
---

## To Do

* [ ] Move all nodes editor html to use modules. [Ref](https://discourse.nodered.org/t/text-javascript-vs-module-in-html/94215/4)
* [ ] Add 🌐 to all uibuilder log messages, before the `[....]`.

### New node: uib-sidebar

* [x] New node to facilitate a sidebar UI [ref](https://github.com/TotallyInformation/node-red-contrib-uibuilder/discussions/510).
* [x] Single node
* [x] Auto-creates sidebar when added to the page.
* [x] Node should use built-in ACE/Monaco editor with a HTML default template to create the main layout.
* [x] All input elements should automatically send data back to the node.
* [x] Input elements should automatically send data to the output port.
* [x] Check if DOMPurify is enabled in the Editor. It is.
* [x] Check if resources/editor-common.{js|css} are available to the tab. They are.
* [ ] If inputs are part of a form, only send the form data when the form is submitted.
* [ ] Incoming msg's should allow multiple `msg.<html-id>` properties that will automatically update the props on the appropriate elements. E.g. `msg.div1.innerHTML` with a value of some HTML should change the HTML content of the div with an id of `div1`.
* [ ] Create a node-red action to display the tab.
* [ ] Apply DOMPurify to incoming HTML content.

#### Consider

* May want to have multiple tabs possible by adding a name setting to the node.
* Might need a flag in the uibuilder setting.js prop that allows/disallows HTML content. Or maybe turns off DOMPurify.
* May want an alternative simpler input msg (as well as the full msg type) with just topic/payload that uses topic for html-id and payload for `value` if it exists on the element or innerText/HTML.

## Answers needed

## Ideas
Expand Down
2 changes: 1 addition & 1 deletion nodes/uib-sidebar/customNode.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@
<div class="form-row node-text-editor-row">
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-editor" ></div>
</div>
<input type="text" id="node-input-html" style="display: none;">
<input type="hidden" id="node-input-html" style="display: none;">
</script>
5 changes: 5 additions & 0 deletions resources/editor-common.css
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,8 @@
.uib-context-select label, .uib-context-select input {
width: 100% !important;
}

/* Sidebar UI content */
#uib-sidebar-ui {
margin: 0.5rem;
}
217 changes: 132 additions & 85 deletions resources/uib-sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,117 +2,166 @@

// Now loading as a module so no need to further Isolate this code

//#region --------- module variables for the panel --------- //

// RED._debug({topic: 'RED.settings', payload:RED.settings})
// NOTE: window.uibuilder is added by editor-common.js - see `resources` folder
const uibuilder = window['uibuilder']
const log = uibuilder.log
/** Module name must match this nodes html file @constant {string} moduleName */
const moduleName = 'uib-sidebar'

// Create a new set to hold all saved node instances oneditsave
if (!window['savedNodes']) {
if (!window['uibSidebarNodes']) {
window['uibSidebarNodes'] = new Set()
}

const sbHTMLx = /*html*/ `
<section id="uib-sidebar-ui" class="uib-sidebar">
<h2>uibuilder Sidebar UI</h2>
<div id="more"></div>
<div>
<button id="uib-send" type="button" onclick="doSend()">Send to Node</button>
</div>
</section>
<script type="module" async >
function sendToNode(node) {
const customMsg = JSON.stringify({ payload: 'Hello from the sidebar' })
const label = node.name || node.id
const postUrl = "/uibuilder/sidebarui/" + node.id
console.log('📊 [uib-sidebar:sidebar] Sending to node runtime:', postUrl, customMsg, node.id, node)
$.ajax({
url: "./uibuilder/sidebarui/" + node.id,
type: "POST",
data: customMsg,
// data: JSON.stringify(customMsg||{}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(
'📊 Sidebar UI send success',
{ type: "success", id: "uib-sidebar", timeout: 2000 }
)
},
error: function (jqXHR, textStatus, errorThrown) {
console.error('📊 ❌ [uib-sidebar:sidebar] POST failed. ', postUrl, errorThrown, textStatus)
RED.notify(
'📊 Failed to send from sidebar UI',
{ type: "error", id: "uib-sidebar" }
)
}
})
}
function doSend() {
window['uibSidebarNodes'].forEach( node => {
console.log('📊 [uib-sidebar:sidebar] Sending data from sidebar to node:', node)
sendToNode(node)
})
/** Send a message via the node's runtime (API call)
* @param {*} node -
* @param {*} msg -
*/
function sendToNode(node, msg) {
msg = JSON.stringify(msg) // needs try/catch
const postUrl = '/uibuilder/sidebarui/' + node.id
// console.log('📊 [uib-sidebar:sidebar] Sending to node runtime:', postUrl, msg, node.id, node)
$.ajax({
url: './uibuilder/sidebarui/' + node.id,
type: 'POST',
data: msg,
contentType: 'application/json; charset=utf-8',
success: function (resp) {
RED.notify(
'📊 Sidebar UI send success',
{ type: 'success', id: moduleName, timeout: 2000 }
)
},
error: function (jqXHR, textStatus, errorThrown) {
console.error('📊 ❌ [uib-sidebar:sidebar] POST failed. ', postUrl, errorThrown, textStatus)
RED.notify(
'📊 Failed to send from sidebar UI',
{ type: 'error', id: moduleName }
)
}
const more = document.getElementById('more')
more.innerText = 'This is the uibuilder sidebar UI'
</script>
`
const sbHTML = /*html*/ `
<section id="uib-sidebar-ui" class="uib-sidebar">
<h2>uibuilder Sidebar UI</h2>
<div id="more"></div>
</section>
`
})
}

const sbMasterEl = document.createElement('section')
sbMasterEl.id = 'uib-sidebar-ui'
sbMasterEl.className = moduleName
let sbEl

// Keep track of the number of uib-sidebar nodes - used for sending msgs from the sidebar
RED.events.on('nodes:add', function(node) {
if (node.type === 'uib-sidebar') {
// When the first uib-sidebar node is added, add the sidebar tab.
if (node.type === moduleName) {
// When the first uib-sidebar node is added ...
if (window['uibSidebarNodes'].size === 0) {
log('📊 [uib-sidebar] FIRST uib-sidebar added - ADDING SIDEBAR')
// Set the default HTML for the sidebar UI
window['uibSidebarHTML'] = node.html ?? '<p>Sidebar UI</p>'
// Add the current node's html to the sbMasterEl
sbMasterEl.innerHTML = window['uibSidebarHTML']
// Add the sidebar tab
RED.sidebar.addTab({
id: 'uibuilder-sidebar-ui',
label: 'uib UI',
name: 'UIBUILDER Sidebar UI',
content: node.html,
content: sbMasterEl,
// toolbar: uiComponents.footer,
enableOnEdit: true,
iconClass: 'fa fa-globe uib-blue',
})
// Get a reference to the sidebar UI element because node-red doesn't add a proper id (as of nr v4.0.8)
sbEl = document.getElementById('uib-sidebar-ui')
}
window['uibSidebarNodes'].add(node)
sbEl.addEventListener('change', function(evt) {
const target = evt.target
// TODO if target is in a form, get all the form data - consider requiring a submit button
const msg = {
payload: target.value,
topic: `${moduleName}/${target.localName}${target.id ? `/${target.id}` : target.name ? `/${target.name}` : ''}`,
from: moduleName,
id: target.id,
name: target.name,
attributes: target.attributes,
data: target.dataset,
willValidate: target.willValidate,
type: target.type,
checked: target.checked,
localName: target.localName,
modifierKeys: {
altKey: evt.altKey,
ctrlKey: evt.ctrlKey,
metaKey: evt.metaKey,
shiftKey: evt.shiftKey,
},
}
// TODO specials for checkboxes and radios
if (!isNaN(target.valueAsNumber)) {
msg.valueAsNumber = target.valueAsNumber
}
if (target.localName === 'select') {
msg.multiple = target.multiple
}
if (target.localName === 'textarea') {
msg.selectionStart = target.selectionStart
msg.selectionEnd = target.selectionEnd
// msg.rows = target.rows
}
log('📊 [uib-sidebar] Input changed:', target, msg)
sendToNode(node, msg)
})
}
})
RED.events.on('nodes:remove', function(node) {
if (node.type === 'uib-sidebar') {
if (node.type === moduleName) {
// Remove the node from the set
window['uibSidebarNodes'].delete(node)
// If there are no more uib-sidebar nodes, remove the sidebar tab
if (window['uibSidebarNodes'].size === 0) {
log('📊 [uib-sidebar] LAST uib-sidebar removed - REMOVING SIDEBAR')
log('📊 [uib-sidebar] LAST uib-sidebar removed - REMOVING SIDEBAR UI')
RED.sidebar.removeTab('uibuilder-sidebar-ui')
}
}
})

/** Update the sidebar UI tab with new HTML content
* @param {string} html - The new HTML to display in the sidebar UI
*/
function updateTab(html) {
RED.sidebar.removeTab('uibuilder-sidebar-ui')
RED.sidebar.addTab({
id: 'uibuilder-sidebar-ui',
label: 'uib UI',
name: 'UIBUILDER Sidebar UI',
content: html,
// toolbar: uiComponents.footer,
enableOnEdit: true,
iconClass: 'fa fa-globe uib-blue',
})
// Empty the current sidebar UI master element
sbEl.innerHTML = ''
// Replace with the new HTML
sbEl.innerHTML = html
}

// NOTE: window.uibuilder is added by editor-common.js - see `resources` folder
const uibuilder = window['uibuilder']
const log = uibuilder.log
/** Module name must match this nodes html file @constant {string} moduleName */
const moduleName = 'uib-sidebar'

//#endregion ------------------------------------------------- //
// Subscribe to notifications from the runtime
RED.comms.subscribe('notification/uibuilder/uib-sidebar/#', function(topic, payload) {
log('📊 [uib-sidebar] Message Received from Sidebar: ', { topic, payload })
const msg = payload
if ('reset' in msg) {
log('📊 [uib-sidebar] Resetting sidebar UI')
// Reset the sidebar UI
updateTab(window['uibSidebarHTML'])
}
if (msg.sidebar) {
// for each entry in msg.sidebar, update the sidebar UI
for (const key in msg.sidebar) {
// log('📊 [uib-sidebar] key:', key, sbEl)
// get a reference to the element with the id of key
const el = sbEl.querySelector(`#${key}`)
// TODO Note that this is rather dangerous as it allows arbitrary HTML to be injected into the sidebar
if (el) {
// for each property in msg.sidebar[key], update the element
for (const prop in msg.sidebar[key]) {
el[prop] = msg.sidebar[key][prop]
}
}
// if (Object.hasOwnProperty.call(msg.sidebar, key)) {
// const html = msg.sidebar[key]
// updateTab(html)
// }
}
}
// TODO Unpack the payload and apply to the sidebar UI
})

//#region --------- module functions for the panel --------- //

Expand All @@ -122,7 +171,8 @@ const moduleName = 'uib-sidebar'
function onEditPrepare(node) {
// log('📊 [uib-sidebar] Edit prepare: node', node)

if (node.html === '') node.html = sbHTML
// In case the html was changed by another uib-sidebar node
if (node.html !== window['uibSidebarHTML']) node.html = window['uibSidebarHTML']

const stateId = RED.editor.generateViewStateId('node', node, '')
node.editor = RED.editor.createEditor({
Expand All @@ -140,6 +190,7 @@ function onEditPrepare(node) {
uibuilder.doTooltips('.ti-edit-panel') // Do this at the end
}

// TODO html from editor has to be GLOBAL, not local to the node
/** Handles the save event when editing a node in the Node-RED editor.
* @param {object} node - The node being edited.
* @description
Expand All @@ -150,7 +201,8 @@ function onEditPrepare(node) {
function onEditSave(node) {
// console.log('uibuilder: uib-sidebar: Edit save: node', node)

const html = node.editor.getValue()
// Update both the node's html property and the global window['uibSidebarHTML'] (for other uib-sidebar nodes)
const html = window['uibSidebarHTML'] = node.editor.getValue()
$('#node-input-html').val(html)

updateTab(html)
Expand Down Expand Up @@ -190,7 +242,7 @@ RED.nodes.registerType(moduleName, {
//#region --- options --- //
defaults: {
name: { value: '' },
html: { value: sbHTML },
html: { value: '' },
// topic: { value: '' },
},
inputs: 1,
Expand Down Expand Up @@ -219,8 +271,3 @@ RED.nodes.registerType(moduleName, {
oneditcancel: function() { onEditCancel(this) },
oneditresize: function(size) { onEditResize(size, this) },
})

// Subscribe to notifications from the runtime
RED.comms.subscribe('notification/uibuilder/uib-sidebar/#', function(topic, payload) {
log('📊 [uib-sidebar] COMMS:SUBSCRIBE Message Received: ', topic, payload)
})

0 comments on commit 7a1a261

Please sign in to comment.