UNPKG

do-red

Version:

A do-node and corresponding return-node for creating loops and task-lists.

586 lines (580 loc) 19.3 kB
/* 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 }