do-red
Version:
A do-node and corresponding return-node for creating loops and task-lists.
586 lines (580 loc) • 19.3 kB
JavaScript
/* global RED */
/* Add rename action */
const renameNode = function () {
// Get the selected node
const selectedNodes = RED.view.selection().nodes
// Ensure that exactly one node is selected
if (!selectedNodes || selectedNodes.length !== 1) {
RED.notify('Select exactly one single node to rename it.', { type: 'warn' })
return
}
const selectedNode = selectedNodes[0]
// Check if it has a name property
if (typeof selectedNode.name !== 'string' && typeof selectedNode._def.defaults.name === 'undefined') {
RED.notify('This node has no name attribute and thus cannot be renamed.', { type: 'warn' })
return
}
// Define action handler
const actionHandler = (e) => {
const oldName = selectedNode.name
const newName = document.getElementById('renameDialogNameInputField').value
// Return if the name didn't change
if (oldName === newName) {
renameDialog.close()
return
}
// Set the new name
selectedNode.name = newName
// Mark node as dirty
selectedNode.dirty = true
selectedNode.resize = true
// Add history entry
RED.history.push({
t: 'edit',
node: selectedNode,
changed: selectedNode.changed,
changes: {
name: oldName
},
dirty: RED.nodes.dirty()
})
// Update workspace
selectedNode.changed = true
RED.nodes.dirty(true)
RED.events.emit('nodes:change', selectedNode)
RED.view.redraw()
// Close dialog
RED.keyboard.remove('escape')
RED.keyboard.remove('enter')
renameDialog.close()
RED.view.focus()
}
// Show popup to edit the name
const renameDialog = RED.notify(
`<p>Set node name</p>
<p>
<div class='form-row'>
<label for='renameDialogNameInputField'>New name</label>
<input type='text' id='renameDialogNameInputField' value='${selectedNodes[0].name || ''}'>
</div>
</p>`,
{
modal: true,
fixed: true,
buttons: [
{
text: 'Cancel',
click: function (e) {
RED.keyboard.remove('escape')
RED.keyboard.remove('enter')
renameDialog.close()
}
},
{
text: 'Rename',
class: 'primary',
click: actionHandler
}
]
}
)
RED.keyboard.add('*', 'escape', function () {
RED.keyboard.remove('escape')
RED.keyboard.remove('enter')
renameDialog.close()
})
RED.keyboard.add('*', 'enter', function () {
actionHandler()
})
document.getElementById('renameDialogNameInputField').select()
setTimeout(() => document.getElementById('renameDialogNameInputField').focus(), 100)
}
const rename = () => RED.actions.add('do:rename-node', renameNode)
RED.keyboard.add('red-ui-workspace', 'ctrl-shift-e', 'do:rename-node')
/* Add replace action */
const replaceNode = function () {
// Get the selected node
const selectedNodes = RED.view.selection().nodes
// Ensure that exactly one node is selected
if (!selectedNodes || selectedNodes.length !== 1) {
RED.notify(
'Select exactly one single node to rename it.',
{
type: 'warn'
}
)
return
}
const selectedNode = selectedNodes[0]
// Show the node-type selection dialog
const nodeView = window.$('#' + selectedNode.id.replace('.', '\\.'))
const position = nodeView.offset()
RED.typeSearch.show({
x: position.left,
y: position.top,
filter: { input: selectedNode.inputs > 0 },
add: (type) => {
const historyEvents = []
// Create replacement node
const newNode = {
type,
id: RED.nodes.id(),
x: selectedNode.x,
y: selectedNode.y,
z: selectedNode.z,
_def: RED.nodes.getType(type),
w: selectedNode.w,
resize: true,
changed: true,
moved: true,
_do_red: 'replace node' // temp identifier for events
}
newNode.inputs = newNode._def.inputs || 0
newNode.outputs = newNode._def.outputs
newNode.h = Math.max(100, (newNode.outputs || 0) * 15)
// Initialize defaults
for (const param in newNode._def.defaults) {
if (newNode._def.defaults?.[param]) {
if (newNode._def.defaults[param].value !== undefined) {
newNode[param] = JSON.parse(JSON.stringify(newNode._def.defaults[param].value))
}
}
}
// Copy values from old node
for (const param in newNode._def.defaults) {
if (newNode?.[param] && selectedNode?.[param] && typeof selectedNode[param] === typeof newNode[param]) {
newNode[param] = selectedNode[param]
}
}
// Copy info and labels
if (typeof selectedNode.info !== 'undefined') {
newNode.info = selectedNode.info
}
if (typeof selectedNode.l !== 'undefined') {
newNode.l = selectedNode.l
}
if (typeof selectedNode.inputLabels !== 'undefined') {
newNode.inputLabels = []
for (let i = 0; i < newNode.inputs; i++) {
if (selectedNode.inputLabels.length > i) {
newNode.inputLabels[i] = selectedNode.inputLabels[i]
}
}
}
if (typeof selectedNode.outputLabels !== 'undefined') {
newNode.outputLabels = []
for (let i = 0; i < newNode.outputs; i++) {
if (selectedNode.outputLabels.length > i) {
newNode.outputLabels[i] = selectedNode.outputLabels[i]
}
}
}
// Call onAdd if present
if (newNode._def.onadd) {
try {
newNode._def.onadd.call(newNode)
} catch (error) {
console.log('Definition error: ' + newNode.type + '.onadd:', error)
}
}
// switch name to old name
newNode.name = selectedNode.name
// Add new node
RED.nodes.add(newNode)
historyEvents.push({
t: 'add',
nodes: [newNode.id]
})
// Get wires from old node
const incomingLinks = RED.nodes.filterLinks({ target: selectedNode })
const outgoingLinks = RED.nodes.filterLinks({ source: selectedNode })
// Remove old links and add new ones
historyEvents.push({
t: 'delete',
links: [
...incomingLinks,
...outgoingLinks
]
})
incomingLinks.forEach(link => {
const newLink = {
source: link.source,
sourcePort: link.sourcePort,
target: RED.nodes.node(newNode.id), // since NR 3 is a Node a ProxyObject
x1: link.x1,
x2: link.x2,
y1: link.y1,
y2: link.y2
}
// RED.nodes.removeLink(link)
RED.nodes.addLink(newLink)
historyEvents.push({
t: 'add',
links: [newLink]
})
})
outgoingLinks.forEach(link => {
// RED.nodes.removeLink(link)
if (link.sourcePort <= newNode.outputs) {
const newLink = {
source: newNode,
sourcePort: link.sourcePort,
target: link.target,
x1: link.x1,
x2: link.x2,
y1: link.y1,
y2: link.y2
}
RED.nodes.addLink(newLink)
historyEvents.push({
t: 'add',
links: [newLink]
})
}
})
// Validate new node
RED.editor.validateNode(newNode)
// Delete old node
RED.nodes.remove(selectedNode.id)
historyEvents.push({
t: 'delete',
nodes: [selectedNode]
})
// Add history
RED.history.push({
t: 'multi',
events: historyEvents,
dirty: RED.nodes.dirty()
})
// Redraw workspace
RED.nodes.dirty(true)
RED.view.updateActive()
RED.view.redraw(true)
RED.view.select(newNode.id)
},
cancel: (e) => {},
move: (e) => {}
})
}
const replace = () => RED.actions.add('do:replace-node-with', replaceNode)
RED.keyboard.add('red-ui-workspace', 'ctrl-shift-r', 'do:replace-node-with')
/* Add append do-node action */
const appendDoNode = function () {
// Get the selected node
const selectedNodes = RED.view.selection().nodes
// Ensure that exactly one node is selected
if (!selectedNodes || selectedNodes.length !== 1) {
RED.notify('Select exactly one single node to which a do-node should be appended.', { type: 'warn' })
return
}
const selectedNode = selectedNodes[0]
// Cancel if the current node has no output
if (selectedNode.outputs === 0) {
return
}
// Create a new node
// const todoRedInstalled = !!RED.nodes.registry.getNodeType('doc-red')
const _def = RED.nodes.getType('do')
// add only meta data here
const newNode = {
type: 'do',
id: RED.nodes.id(),
x: selectedNode.x + selectedNode.w + (4 * RED.view.gridSize()),
y: selectedNode.y,
z: selectedNode.z,
_def,
w: selectedNode.w,
resize: true,
changed: true,
moved: true,
valid: true,
inputs: _def.inputs || 0,
outputs: _def.outputs,
h: Math.max(100, (_def.outputs || 0) * 15),
_do_red: 'append node' // temp identifier for events
}
// Initialize defaults
for (const param in newNode._def.defaults) {
if (newNode._def.defaults[param].value !== undefined) {
newNode[param] = JSON.parse(JSON.stringify(newNode._def.defaults[param].value))
}
}
// add additional node data
newNode._version = _def.set.version
RED.nodes.add(newNode)
// RED.events.emit('nodes:add', newNode) // happens through RED.nodes.add()
// Determine source port (first free port or first port)
const ports = [...Array(selectedNode.outputs).keys()]
const outgoingLinks = RED.nodes.filterLinks({ source: selectedNode })
const usedPorts = []
outgoingLinks.forEach(link => {
const portNumber = link.sourcePort
if (!usedPorts.includes(portNumber)) {
usedPorts.push(portNumber)
}
})
const freePorts = ports.filter(port => !usedPorts.includes(port))
const portNumber = freePorts.length > 0 ? freePorts[0] : 0
// Create wire
const link = {
source: selectedNode,
sourcePort: portNumber,
target: RED.nodes.node(newNode.id) // since NR 3 is a Node a ProxyObject
}
RED.nodes.addLink(link)
// Add history event
RED.history.push({
t: 'add',
nodes: [newNode.id],
links: [link],
dirty: RED.nodes.dirty()
})
// Redraw workspace
RED.nodes.dirty(true)
RED.view.updateActive()
RED.view.redraw()
RED.view.select(newNode.id)
// Update breadcrumbs if necessary
if (currentNode === selectedNode.id) {
breadcrumbs = [...breadcrumbs.slice(0, currentIndex + 1), newNode.id]
currentNode = newNode.id
currentIndex = breadcrumbs.length
}
// Run rename node
renameNode()
}
const appendNode = () => RED.actions.add('do:append-do-node', appendDoNode)
RED.keyboard.add('red-ui-workspace', 'ctrl-shift-a', 'do:append-do-node')
// Add move-cursor actions
const directions = {
left: 'left',
right: 'right',
up: 'up',
down: 'down'
}
let forward = true // Determines whether we are going forwards or backwards (important for going up and down afterwards)
let breadcrumbs = []
let currentIndex = 0
let currentNode
const outgoingSorter = (a, b) => {
const portDiff = a.sourcePort - b.sourcePort
if (portDiff !== 0) {
return portDiff
} else {
return a.y2 - b.y2
}
}
const moveDoCursor = function (to) {
// Get the selected node
const selectedNodes = RED.view.selection().nodes
// Ensure that exactly one node is selected
if (!selectedNodes || selectedNodes.length !== 1) {
return
}
const selectedNode = selectedNodes[0]
// If the current node has changed e.g. due to a mouse click, we need to reset everything
if (currentNode !== selectedNode.id) {
currentNode = selectedNode.id
breadcrumbs = [currentNode]
currentIndex = 0
}
// Get wires
const incomingLinks = RED.nodes.filterLinks({ target: selectedNode })
const outgoingLinks = RED.nodes.filterLinks({ source: selectedNode })
// Move cursor
if (to === directions.left) {
forward = false
// Check the breadcrumbs if we visited a node left of here before
let gotoNode
if (currentIndex === 0) {
// Sort the links by y-axis
const sortedLinks = incomingLinks.sort((a, b) => a.y1 - b.y1)
if (sortedLinks.length === 0) {
return
}
// We never were left of here before -> take the upper most link
gotoNode = sortedLinks[0].source.id
breadcrumbs = [gotoNode, ...breadcrumbs]
} else {
// Take the index left of "here"
currentIndex = currentIndex - 1
gotoNode = breadcrumbs[currentIndex]
}
// Change selection
RED.view.select(gotoNode)
currentNode = gotoNode
} else if (to === directions.right) {
forward = true
// Check the breadcrumbs if we visited a node left of here before
let gotoNode
if (currentIndex === breadcrumbs.length - 1) {
// Sort the links by port
const sortedLinks = outgoingLinks.sort(outgoingSorter)
if (sortedLinks.length === 0) {
return
}
// We never were right of here before -> take the first port
gotoNode = sortedLinks[0].target.id
breadcrumbs = [...breadcrumbs, gotoNode]
} else {
// Take the index right of "here"
gotoNode = breadcrumbs[currentIndex + 1]
}
currentIndex = currentIndex + 1
// Change selection
RED.view.select(gotoNode)
currentNode = gotoNode
} else if (to === directions.up) {
// Are we going forward or backward?
if (forward) {
// Get previous node from breadcrumbs
if (currentIndex === 0) {
const sortedLinks = incomingLinks.sort((a, b) => a.y1 - b.y1)
if (sortedLinks.length === 0) {
return
}
const previousNode = sortedLinks[0].source.id
breadcrumbs = [previousNode, ...breadcrumbs]
currentIndex = 1
}
const previousNode = breadcrumbs[currentIndex - 1]
RED.view.reveal(previousNode)
// Get next in list
const outgoingLinks = RED.nodes.filterLinks({ source: RED.nodes.node(previousNode) })
const sortedLinks = outgoingLinks.sort(outgoingSorter)
for (let i = 0; i < sortedLinks.length; i++) {
if (sortedLinks[i].target === selectedNode) {
let gotoNode = sortedLinks[i].target.id
if (i > 0) {
gotoNode = sortedLinks[i - 1].target.id
}
// Select node
breadcrumbs[currentIndex] = gotoNode
RED.view.select(gotoNode)
currentNode = gotoNode
// Cut breadcrumbs
breadcrumbs = breadcrumbs.slice(0, currentIndex + 1)
return
}
}
RED.view.select(breadcrumbs[currentIndex])
} else {
// Get next node from breadcrumbs
if (currentIndex === breadcrumbs.length - 1) {
const sortedLinks = outgoingLinks.sort(outgoingSorter)
if (sortedLinks.length === 0) {
return
}
const nextNode = sortedLinks[0].source.id
breadcrumbs = [...breadcrumbs, nextNode]
currentIndex = currentIndex + 1
}
const nextNode = breadcrumbs[currentIndex + 1]
RED.view.reveal(nextNode)
// Get next in list
const incomingLinks = RED.nodes.filterLinks({ target: RED.nodes.node(nextNode) })
const sortedLinks = incomingLinks.sort((a, b) => a.y1 - b.y1)
for (let i = 0; i < sortedLinks.length; i++) {
if (sortedLinks[i].source === selectedNode) {
let gotoNode = sortedLinks[i].source.id
if (i > 0) {
gotoNode = sortedLinks[i - 1].source.id
}
// Select node
breadcrumbs[currentIndex] = gotoNode
RED.view.select(gotoNode)
currentNode = gotoNode
// Cut breadcrumbs
breadcrumbs = breadcrumbs.slice(currentIndex)
currentIndex = 0
return
}
}
RED.view.select(breadcrumbs[currentIndex])
}
} else if (to === directions.down) {
// Are we going forward or backward?
if (forward) {
// Get previous node from breadcrumbs
if (currentIndex === 0) {
const sortedLinks = incomingLinks.sort((a, b) => a.y1 - b.y1)
if (sortedLinks.length === 0) {
return
}
const previousNode = sortedLinks[0].source.id
breadcrumbs = [previousNode, ...breadcrumbs]
currentIndex = 1
}
const previousNode = breadcrumbs[currentIndex - 1]
RED.view.reveal(previousNode)
// Get next in list
const outgoingLinks = RED.nodes.filterLinks({ source: RED.nodes.node(previousNode) })
const sortedLinks = outgoingLinks.sort(outgoingSorter)
for (let i = 0; i < sortedLinks.length; i++) {
if (sortedLinks[i].target === selectedNode) {
let gotoNode = sortedLinks[i].target.id
if (sortedLinks.length > i + 1) {
gotoNode = sortedLinks[i + 1].target.id
}
// Select node
breadcrumbs[currentIndex] = gotoNode
RED.view.select(gotoNode)
currentNode = gotoNode
// Cut breadcrumbs
breadcrumbs = breadcrumbs.slice(0, currentIndex + 1)
return
}
}
RED.view.select(breadcrumbs[currentIndex])
} else {
// Get next node from breadcrumbs
if (currentIndex === breadcrumbs.length - 1) {
const sortedLinks = outgoingLinks.sort(outgoingSorter)
if (sortedLinks.length === 0) {
return
}
const nextNode = sortedLinks[0].source.id
breadcrumbs = [...breadcrumbs, nextNode]
currentIndex = currentIndex + 1
}
const nextNode = breadcrumbs[currentIndex + 1]
RED.view.reveal(nextNode)
// Get next in list
const incomingLinks = RED.nodes.filterLinks({ target: RED.nodes.node(nextNode) })
const sortedLinks = incomingLinks.sort((a, b) => a.y1 - b.y1)
for (let i = 0; i < sortedLinks.length; i++) {
if (sortedLinks[i].source === selectedNode) {
let gotoNode = sortedLinks[i].source.id
if (sortedLinks.length > i + 1) {
gotoNode = sortedLinks[i + 1].source.id
}
// Select node
breadcrumbs[currentIndex] = gotoNode
RED.view.select(gotoNode)
currentNode = gotoNode
// Cut breadcrumbs
breadcrumbs = breadcrumbs.slice(currentIndex)
currentIndex = 0
return
}
}
RED.view.select(breadcrumbs[currentIndex])
}
}
}
const moveCursor = () => {
RED.actions.add('do:move-cursor-left', () => moveDoCursor(directions.left))
RED.actions.add('do:move-cursor-right', () => moveDoCursor(directions.right))
RED.actions.add('do:move-cursor-up', () => moveDoCursor(directions.up))
RED.actions.add('do:move-cursor-down', () => moveDoCursor(directions.down))
RED.keyboard.add('red-ui-workspace', 'ctrl-shift-left', 'do:move-cursor-left')
RED.keyboard.add('red-ui-workspace', 'ctrl-shift-right', 'do:move-cursor-right')
RED.keyboard.add('red-ui-workspace', 'ctrl-shift-up', 'do:move-cursor-up')
RED.keyboard.add('red-ui-workspace', 'ctrl-shift-down', 'do:move-cursor-down')
}
module.exports = {
rename,
replace,
appendNode,
moveCursor
}