2 * Copyright 2013, 2014 IBM Corp.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
18 RED.view = (function() {
19 var space_width = 5000,
21 lineCurveScale = 0.75,
26 var touchLongPressTimeout = 1000,
27 startTouchDistance = 0,
28 startTouchCenter = [],
33 var activeWorkspace = 0;
34 var workspaceScrollPositions = {};
36 var selected_link = null,
37 mousedown_link = null,
38 mousedown_node = null,
39 mousedown_port_type = null,
40 mousedown_port_index = 0,
43 mouse_position = null,
50 dblClickPrimed = null,
56 var status_colours = {
64 var outer = d3.select("#chart")
66 .attr("width", space_width)
67 .attr("height", space_height)
68 .attr("pointer-events", "all")
69 .style("cursor","crosshair");
73 .on("dblclick.zoom", null)
75 .on("mousemove", canvasMouseMove)
76 .on("mousedown", canvasMouseDown)
77 .on("mouseup", canvasMouseUp)
78 .on("touchend", function() {
79 clearTimeout(touchStartTime);
80 touchStartTime = null;
81 if (RED.touch.radialMenu.active()) {
85 outer_background.attr("fill","#fff");
87 canvasMouseUp.call(this);
89 .on("touchcancel", canvasMouseUp)
90 .on("touchstart", function() {
92 if (d3.event.touches.length>1) {
93 clearTimeout(touchStartTime);
94 touchStartTime = null;
95 d3.event.preventDefault();
96 touch0 = d3.event.touches.item(0);
97 var touch1 = d3.event.touches.item(1);
98 var a = touch0['pageY']-touch1['pageY'];
99 var b = touch0['pageX']-touch1['pageX'];
101 var offset = $("#chart").offset();
102 var scrollPos = [$("#chart").scrollLeft(),$("#chart").scrollTop()];
104 (touch1['pageX']+(b/2)-offset.left+scrollPos[0])/scaleFactor,
105 (touch1['pageY']+(a/2)-offset.top+scrollPos[1])/scaleFactor
108 touch1['pageX']+(b/2),
109 touch1['pageY']+(a/2)
111 startTouchDistance = Math.sqrt((a*a)+(b*b));
113 var obj = d3.select(document.body);
114 touch0 = d3.event.touches.item(0);
115 var pos = [touch0.pageX,touch0.pageY];
116 startTouchCenter = [touch0.pageX,touch0.pageY];
117 startTouchDistance = 0;
118 var point = d3.touches(this)[0];
119 touchStartTime = setTimeout(function() {
120 touchStartTime = null;
121 showTouchMenu(obj,pos);
122 //lasso = vis.append('rect')
123 // .attr("ox",point[0])
124 // .attr("oy",point[1])
127 // .attr("x",point[0])
128 // .attr("y",point[1])
131 // .attr("class","lasso");
132 //outer_background.attr("fill","#e3e3f3");
133 },touchLongPressTimeout);
136 .on("touchmove", function(){
137 if (RED.touch.radialMenu.active()) {
138 d3.event.preventDefault();
142 if (d3.event.touches.length<2) {
143 if (touchStartTime) {
144 touch0 = d3.event.touches.item(0);
145 var dx = (touch0.pageX-startTouchCenter[0]);
146 var dy = (touch0.pageY-startTouchCenter[1]);
147 var d = Math.abs(dx*dx+dy*dy);
149 clearTimeout(touchStartTime);
150 touchStartTime = null;
153 d3.event.preventDefault();
155 canvasMouseMove.call(this);
157 touch0 = d3.event.touches.item(0);
158 var touch1 = d3.event.touches.item(1);
159 var a = touch0['pageY']-touch1['pageY'];
160 var b = touch0['pageX']-touch1['pageX'];
161 var offset = $("#chart").offset();
162 var scrollPos = [$("#chart").scrollLeft(),$("#chart").scrollTop()];
163 var moveTouchDistance = Math.sqrt((a*a)+(b*b));
165 touch1['pageX']+(b/2),
166 touch1['pageY']+(a/2)
169 if (!isNaN(moveTouchDistance)) {
170 oldScaleFactor = scaleFactor;
171 scaleFactor = Math.min(2,Math.max(0.3, scaleFactor + (Math.floor(((moveTouchDistance*100)-(startTouchDistance*100)))/10000)));
173 var deltaTouchCenter = [ // Try to pan whilst zooming - not 100%
174 startTouchCenter[0]*(scaleFactor-oldScaleFactor),//-(touchCenter[0]-moveTouchCenter[0]),
175 startTouchCenter[1]*(scaleFactor-oldScaleFactor) //-(touchCenter[1]-moveTouchCenter[1])
178 startTouchDistance = moveTouchDistance;
179 moveTouchCenter = touchCenter;
181 $("#chart").scrollLeft(scrollPos[0]+deltaTouchCenter[0]);
182 $("#chart").scrollTop(scrollPos[1]+deltaTouchCenter[1]);
188 var outer_background = vis.append('svg:rect')
189 .attr('width', space_width)
190 .attr('height', space_height)
191 .attr('fill','#fff');
193 //var gridScale = d3.scale.linear().range([0,2000]).domain([0,2000]);
194 //var grid = vis.append('g');
196 //grid.selectAll("line.horizontal").data(gridScale.ticks(100)).enter()
200 // "class":"horizontal",
203 // "y1" : function(d){ return gridScale(d);},
204 // "y2" : function(d){ return gridScale(d);},
206 // "shape-rendering" : "crispEdges",
207 // "stroke" : "#eee",
208 // "stroke-width" : "1px"
210 //grid.selectAll("line.vertical").data(gridScale.ticks(100)).enter()
214 // "class":"vertical",
217 // "x1" : function(d){ return gridScale(d);},
218 // "x2" : function(d){ return gridScale(d);},
220 // "shape-rendering" : "crispEdges",
221 // "stroke" : "#eee",
222 // "stroke-width" : "1px"
226 var drag_line = vis.append("svg:path").attr("class", "drag_line");
228 var workspace_tabs = RED.tabs.create({
229 id: "workspace-tabs",
230 onchange: function(tab) {
231 if (tab.type == "subflow") {
232 $("#workspace-toolbar").show();
234 $("#workspace-toolbar").hide();
236 var chart = $("#chart");
237 if (activeWorkspace !== 0) {
238 workspaceScrollPositions[activeWorkspace] = {
239 left:chart.scrollLeft(),
240 top:chart.scrollTop()
243 var scrollStartLeft = chart.scrollLeft();
244 var scrollStartTop = chart.scrollTop();
246 activeWorkspace = tab.id;
247 if (workspaceScrollPositions[activeWorkspace]) {
248 chart.scrollLeft(workspaceScrollPositions[activeWorkspace].left);
249 chart.scrollTop(workspaceScrollPositions[activeWorkspace].top);
254 var scrollDeltaLeft = chart.scrollLeft() - scrollStartLeft;
255 var scrollDeltaTop = chart.scrollTop() - scrollStartTop;
256 if (mouse_position != null) {
257 mouse_position[0] += scrollDeltaLeft;
258 mouse_position[1] += scrollDeltaTop;
262 RED.nodes.eachNode(function(n) {
267 ondblclick: function(tab) {
268 showRenameWorkspaceDialog(tab.id);
270 onadd: function(tab) {
271 RED.menu.addItem("btn-workspace-menu",{
272 id:"btn-workspace-menu-"+tab.id.replace(".","-"),
274 onselect:function() {
275 workspace_tabs.activateTab(tab.id);
278 RED.menu.setDisabled("btn-workspace-delete",workspace_tabs.count() == 1);
280 onremove: function(tab) {
281 RED.menu.setDisabled("btn-workspace-delete",workspace_tabs.count() == 1);
282 RED.menu.removeItem("btn-workspace-menu-"+tab.id.replace(".","-"));
286 var workspaceIndex = 0;
288 function addWorkspace() {
289 var tabId = RED.nodes.id();
292 } while($("#workspace-tabs a[title='Sheet "+workspaceIndex+"']").size() !== 0);
294 var ws = {type:"tab",id:tabId,label:"Sheet "+workspaceIndex};
295 RED.nodes.addWorkspace(ws);
296 workspace_tabs.addTab(ws);
297 workspace_tabs.activateTab(tabId);
298 RED.history.push({t:'add',workspaces:[ws],dirty:dirty});
299 RED.view.dirty(true);
302 $('#btn-workspace-add-tab').on("click",addWorkspace);
303 $('#btn-workspace-add').on("click",addWorkspace);
304 $('#btn-workspace-edit').on("click",function() {
305 showRenameWorkspaceDialog(activeWorkspace);
307 $('#btn-workspace-delete').on("click",function() {
308 deleteWorkspace(activeWorkspace);
312 function deleteWorkspace(id) {
313 if (workspace_tabs.count() == 1) {
316 var ws = RED.nodes.workspace(id);
317 $( "#node-dialog-delete-workspace" ).dialog('option','workspace',ws);
318 $( "#node-dialog-delete-workspace-name" ).text(ws.label);
319 $( "#node-dialog-delete-workspace" ).dialog('open');
322 function canvasMouseDown() {
323 if (!mousedown_node && !mousedown_link) {
324 selected_link = null;
327 if (mouse_mode === 0) {
333 if (!touchStartTime) {
334 var point = d3.mouse(this);
335 lasso = vis.append('rect')
344 .attr("class","lasso");
345 d3.event.preventDefault();
350 function canvasMouseMove() {
351 mouse_position = d3.touches(this)[0]||d3.mouse(this);
353 // Prevent touch scrolling...
354 //if (d3.touches(this)[0]) {
355 // d3.event.preventDefault();
358 // TODO: auto scroll the container
359 //var point = d3.mouse(this);
360 //if (point[0]-container.scrollLeft < 30 && container.scrollLeft > 0) { container.scrollLeft -= 15; }
361 //console.log(d3.mouse(this),container.offsetWidth,container.offsetHeight,container.scrollLeft,container.scrollTop);
364 var ox = parseInt(lasso.attr("ox"));
365 var oy = parseInt(lasso.attr("oy"));
366 var x = parseInt(lasso.attr("x"));
367 var y = parseInt(lasso.attr("y"));
370 if (mouse_position[0] < ox) {
371 x = mouse_position[0];
374 w = mouse_position[0]-x;
376 if (mouse_position[1] < oy) {
377 y = mouse_position[1];
380 h = mouse_position[1]-y;
391 if (mouse_mode != RED.state.IMPORT_DRAGGING && !mousedown_node && selected_link == null) {
396 if (mouse_mode == RED.state.JOINING) {
398 drag_line.attr("class", "drag_line");
399 mousePos = mouse_position;
400 var numOutputs = (mousedown_port_type === 0)?(mousedown_node.outputs || 1):1;
401 var sourcePort = mousedown_port_index;
402 var portY = -((numOutputs-1)/2)*13 +13*sourcePort;
404 var sc = (mousedown_port_type === 0)?1:-1;
406 var dy = mousePos[1]-(mousedown_node.y+portY);
407 var dx = mousePos[0]-(mousedown_node.x+sc*mousedown_node.w/2);
408 var delta = Math.sqrt(dy*dy+dx*dx);
409 var scale = lineCurveScale;
412 if (delta < node_width) {
413 scale = 0.75-0.75*((node_width-delta)/node_width);
416 scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width));
417 if (Math.abs(dy) < 3*node_height) {
418 scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ;
423 "M "+(mousedown_node.x+sc*mousedown_node.w/2)+" "+(mousedown_node.y+portY)+
424 " C "+(mousedown_node.x+sc*(mousedown_node.w/2+node_width*scale))+" "+(mousedown_node.y+portY+scaleY*node_height)+" "+
425 (mousePos[0]-sc*(scale)*node_width)+" "+(mousePos[1]-scaleY*node_height)+" "+
426 mousePos[0]+" "+mousePos[1]
428 d3.event.preventDefault();
429 } else if (mouse_mode == RED.state.MOVING) {
430 mousePos = mouse_position;
431 var d = (mouse_offset[0]-mousePos[0])*(mouse_offset[0]-mousePos[0]) + (mouse_offset[1]-mousePos[1])*(mouse_offset[1]-mousePos[1]);
433 mouse_mode = RED.state.MOVING_ACTIVE;
436 } else if (mouse_mode == RED.state.MOVING_ACTIVE || mouse_mode == RED.state.IMPORT_DRAGGING) {
437 mousePos = mouse_position;
442 for (var n = 0; n<moving_set.length; n++) {
443 node = moving_set[n];
444 if (d3.event.shiftKey) {
445 node.n.ox = node.n.x;
446 node.n.oy = node.n.y;
448 node.n.x = mousePos[0]+node.dx;
449 node.n.y = mousePos[1]+node.dy;
451 minX = Math.min(node.n.x-node.n.w/2-5,minX);
452 minY = Math.min(node.n.y-node.n.h/2-5,minY);
454 if (minX !== 0 || minY !== 0) {
455 for (i = 0; i<moving_set.length; i++) {
456 node = moving_set[i];
461 if (d3.event.shiftKey && moving_set.length > 0) {
462 var gridOffset = [0,0];
463 node = moving_set[0];
464 gridOffset[0] = node.n.x-(20*Math.floor((node.n.x-node.n.w/2)/20)+node.n.w/2);
465 gridOffset[1] = node.n.y-(20*Math.floor(node.n.y/20));
466 if (gridOffset[0] !== 0 || gridOffset[1] !== 0) {
467 for (i = 0; i<moving_set.length; i++) {
468 node = moving_set[i];
469 node.n.x -= gridOffset[0];
470 node.n.y -= gridOffset[1];
471 if (node.n.x == node.n.ox && node.n.y == node.n.oy) {
481 function canvasMouseUp() {
482 if (mousedown_node && mouse_mode == RED.state.JOINING) {
483 drag_line.attr("class", "drag_line_hidden");
486 var x = parseInt(lasso.attr("x"));
487 var y = parseInt(lasso.attr("y"));
488 var x2 = x+parseInt(lasso.attr("width"));
489 var y2 = y+parseInt(lasso.attr("height"));
490 if (!d3.event.ctrlKey) {
493 RED.nodes.eachNode(function(n) {
494 if (n.z == activeWorkspace && !n.selected) {
495 n.selected = (n.x > x && n.x < x2 && n.y > y && n.y < y2);
498 moving_set.push({n:n});
505 } else if (mouse_mode == RED.state.DEFAULT && mousedown_link == null && !d3.event.ctrlKey ) {
509 if (mouse_mode == RED.state.MOVING_ACTIVE) {
510 if (moving_set.length > 0) {
512 for (var j=0;j<moving_set.length;j++) {
513 ns.push({n:moving_set[j].n,ox:moving_set[j].ox,oy:moving_set[j].oy});
515 RED.history.push({t:'move',nodes:ns,dirty:dirty});
518 if (mouse_mode == RED.state.MOVING || mouse_mode == RED.state.MOVING_ACTIVE) {
519 for (var i=0;i<moving_set.length;i++) {
520 delete moving_set[i].ox;
521 delete moving_set[i].oy;
524 if (mouse_mode == RED.state.IMPORT_DRAGGING) {
525 RED.keyboard.remove(/* ESCAPE */ 27);
529 // clear mouse event vars
533 $('#btn-zoom-out').click(function() {zoomOut();});
534 $('#btn-zoom-zero').click(function() {zoomZero();});
535 $('#btn-zoom-in').click(function() {zoomIn();});
536 $("#chart").on('DOMMouseScroll mousewheel', function (evt) {
538 evt.preventDefault();
539 evt.stopPropagation();
540 var move = -(evt.originalEvent.detail) || evt.originalEvent.wheelDelta;
541 if (move <= 0) { zoomOut(); }
545 $("#chart").droppable({
546 accept:".palette_node",
547 drop: function( event, ui ) {
549 var selected_tool = ui.draggable[0].type;
550 var mousePos = d3.touches(this)[0]||d3.mouse(this);
551 mousePos[1] += this.scrollTop;
552 mousePos[0] += this.scrollLeft;
553 mousePos[1] /= scaleFactor;
554 mousePos[0] /= scaleFactor;
556 var nn = { id:(1+Math.random()*4294967295).toString(16),x: mousePos[0],y:mousePos[1],w:node_width,z:activeWorkspace};
558 nn.type = selected_tool;
559 nn._def = RED.nodes.getType(nn.type);
560 nn.outputs = nn._def.outputs;
563 for (var d in nn._def.defaults) {
564 if (nn._def.defaults.hasOwnProperty(d)) {
565 nn[d] = nn._def.defaults[d].value;
570 nn._def.onadd.call(nn);
573 nn.h = Math.max(node_height,(nn.outputs||0) * 15);
574 RED.history.push({t:'add',nodes:[nn.id],dirty:dirty});
576 RED.editor.validateNode(nn);
578 // auto select dropped node - so info shows (if visible)
581 moving_set.push({n:nn});
585 if (nn._def.autoedit) {
592 if (scaleFactor < 2) {
598 if (scaleFactor > 0.3) {
603 function zoomZero() {
608 function selectAll() {
609 RED.nodes.eachNode(function(n) {
610 if (n.z == activeWorkspace) {
614 moving_set.push({n:n});
618 selected_link = null;
623 function clearSelection() {
624 for (var i=0;i<moving_set.length;i++) {
625 var n = moving_set[i];
627 n.n.selected = false;
630 selected_link = null;
633 function updateSelection() {
634 if (moving_set.length === 0) {
635 RED.menu.setDisabled("btn-export-menu",true);
636 RED.menu.setDisabled("btn-export-clipboard",true);
637 RED.menu.setDisabled("btn-export-library",true);
639 RED.menu.setDisabled("btn-export-menu",false);
640 RED.menu.setDisabled("btn-export-clipboard",false);
641 RED.menu.setDisabled("btn-export-library",false);
643 if (moving_set.length === 0 && selected_link == null) {
644 RED.keyboard.remove(/* backspace */ 8);
645 RED.keyboard.remove(/* delete */ 46);
646 RED.keyboard.remove(/* c */ 67);
647 RED.keyboard.remove(/* x */ 88);
649 RED.keyboard.add(/* backspace */ 8,function(){deleteSelection();d3.event.preventDefault();});
650 RED.keyboard.add(/* delete */ 46,function(){deleteSelection();d3.event.preventDefault();});
651 RED.keyboard.add(/* c */ 67,{ctrl:true},function(){copySelection();d3.event.preventDefault();});
652 RED.keyboard.add(/* x */ 88,{ctrl:true},function(){copySelection();deleteSelection();d3.event.preventDefault();});
654 if (moving_set.length === 0) {
655 RED.keyboard.remove(/* up */ 38);
656 RED.keyboard.remove(/* down */ 40);
657 RED.keyboard.remove(/* left */ 37);
658 RED.keyboard.remove(/* right*/ 39);
660 RED.keyboard.add(/* up */ 38, function() { if(d3.event.shiftKey){moveSelection( 0,-20)}else{moveSelection( 0,-1);}d3.event.preventDefault();},endKeyboardMove);
661 RED.keyboard.add(/* down */ 40, function() { if(d3.event.shiftKey){moveSelection( 0, 20)}else{moveSelection( 0, 1);}d3.event.preventDefault();},endKeyboardMove);
662 RED.keyboard.add(/* left */ 37, function() { if(d3.event.shiftKey){moveSelection(-20, 0)}else{moveSelection(-1, 0);}d3.event.preventDefault();},endKeyboardMove);
663 RED.keyboard.add(/* right*/ 39, function() { if(d3.event.shiftKey){moveSelection( 20, 0)}else{moveSelection( 1, 0);}d3.event.preventDefault();},endKeyboardMove);
665 if (moving_set.length == 1) {
666 RED.sidebar.info.refresh(moving_set[0].n);
668 RED.sidebar.info.clear();
671 function endKeyboardMove() {
673 for (var i=0;i<moving_set.length;i++) {
674 ns.push({n:moving_set[i].n,ox:moving_set[i].ox,oy:moving_set[i].oy});
675 delete moving_set[i].ox;
676 delete moving_set[i].oy;
678 RED.history.push({t:'move',nodes:ns,dirty:dirty});
680 function moveSelection(dx,dy) {
685 for (var i=0;i<moving_set.length;i++) {
686 node = moving_set[i];
687 if (node.ox == null && node.oy == null) {
694 minX = Math.min(node.n.x-node.n.w/2-5,minX);
695 minY = Math.min(node.n.y-node.n.h/2-5,minY);
698 if (minX !== 0 || minY !== 0) {
699 for (var n = 0; n<moving_set.length; n++) {
700 node = moving_set[n];
708 function deleteSelection() {
709 var removedNodes = [];
710 var removedLinks = [];
711 var startDirty = dirty;
712 if (moving_set.length > 0) {
713 for (var i=0;i<moving_set.length;i++) {
714 var node = moving_set[i].n;
715 node.selected = false;
719 var rmlinks = RED.nodes.remove(node.id);
720 removedNodes.push(node);
721 removedLinks = removedLinks.concat(rmlinks);
727 RED.nodes.removeLink(selected_link);
728 removedLinks.push(selected_link);
731 RED.history.push({t:'delete',nodes:removedNodes,links:removedLinks,dirty:startDirty});
733 selected_link = null;
738 function copySelection() {
739 if (moving_set.length > 0) {
741 for (var n=0;n<moving_set.length;n++) {
742 var node = moving_set[n].n;
743 nns.push(RED.nodes.convertNode(node));
745 clipboard = JSON.stringify(nns);
746 RED.notify(moving_set.length+" node"+(moving_set.length>1?"s":"")+" copied");
751 function calculateTextWidth(str) {
752 var sp = document.createElement("span");
753 sp.className = "node_label";
754 sp.style.position = "absolute";
755 sp.style.top = "-1000px";
756 sp.innerHTML = (str||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
757 document.body.appendChild(sp);
758 var w = sp.offsetWidth;
759 document.body.removeChild(sp);
763 function resetMouseVars() {
764 mousedown_node = null;
766 mousedown_link = null;
768 mousedown_port_type = 0;
771 function portMouseDown(d,portType,portIndex) {
773 //vis.call(d3.behavior.zoom().on("zoom"), null);
775 selected_link = null;
776 mouse_mode = RED.state.JOINING;
777 mousedown_port_type = portType;
778 mousedown_port_index = portIndex || 0;
779 document.body.style.cursor = "crosshair";
780 d3.event.preventDefault();
783 function portMouseUp(d,portType,portIndex) {
784 document.body.style.cursor = "";
785 if (mouse_mode == RED.state.JOINING && mousedown_node) {
786 if (typeof TouchEvent != "undefined" && d3.event instanceof TouchEvent) {
787 RED.nodes.eachNode(function(n) {
788 if (n.z == activeWorkspace) {
791 if (n.x-hw<mouse_position[0] && n.x+hw> mouse_position[0] &&
792 n.y-hh<mouse_position[1] && n.y+hh>mouse_position[1]) {
794 portType = mouseup_node._def.inputs>0?1:0;
802 if (portType == mousedown_port_type || mouseup_node === mousedown_node) {
803 drag_line.attr("class", "drag_line_hidden");
807 var src,dst,src_port;
808 if (mousedown_port_type === 0) {
809 src = mousedown_node;
810 src_port = mousedown_port_index;
812 } else if (mousedown_port_type == 1) {
814 dst = mousedown_node;
815 src_port = portIndex;
818 var existingLink = false;
819 RED.nodes.eachLink(function(d) {
820 existingLink = existingLink || (d.source === src && d.target === dst && d.sourcePort == src_port);
823 var link = {source: src, sourcePort:src_port, target: dst};
824 RED.nodes.addLink(link);
825 RED.history.push({t:'add',links:[link],dirty:dirty});
828 selected_link = null;
833 function nodeMouseUp(d) {
834 if (dblClickPrimed && mousedown_node == d && clickElapsed > 0 && clickElapsed < 750) {
837 d3.event.stopPropagation();
840 portMouseUp(d, d._def.inputs > 0 ? 1 : 0, 0);
843 function nodeMouseDown(d) {
844 //var touch0 = d3.event;
845 //var pos = [touch0.pageX,touch0.pageY];
846 //RED.touch.radialMenu.show(d3.select(this),pos);
847 if (mouse_mode == RED.state.IMPORT_DRAGGING) {
848 RED.keyboard.remove(/* ESCAPE */ 27);
853 d3.event.stopPropagation();
857 var now = Date.now();
858 clickElapsed = now-clickTime;
861 dblClickPrimed = (lastClickNode == mousedown_node);
862 lastClickNode = mousedown_node;
866 if (d.selected && d3.event.ctrlKey) {
868 for (i=0;i<moving_set.length;i+=1) {
869 if (moving_set[i].n === d) {
870 moving_set.splice(i,1);
875 if (d3.event.shiftKey) {
877 var cnodes = RED.nodes.getAllFlowNodes(mousedown_node);
878 for (var n=0;n<cnodes.length;n++) {
879 cnodes[n].selected = true;
880 cnodes[n].dirty = true;
881 moving_set.push({n:cnodes[n]});
883 } else if (!d.selected) {
884 if (!d3.event.ctrlKey) {
887 mousedown_node.selected = true;
888 moving_set.push({n:mousedown_node});
890 selected_link = null;
891 if (d3.event.button != 2) {
892 mouse_mode = RED.state.MOVING;
893 var mouse = d3.touches(this)[0]||d3.mouse(this);
894 mouse[0] += d.x-d.w/2;
895 mouse[1] += d.y-d.h/2;
896 for (i=0;i<moving_set.length;i++) {
897 moving_set[i].ox = moving_set[i].n.x;
898 moving_set[i].oy = moving_set[i].n.y;
899 moving_set[i].dx = moving_set[i].n.x-mouse[0];
900 moving_set[i].dy = moving_set[i].n.y-mouse[1];
902 mouse_offset = d3.mouse(document.body);
903 if (isNaN(mouse_offset[0])) {
904 mouse_offset = d3.touches(document.body)[0];
911 d3.event.stopPropagation();
914 function nodeButtonClicked(d) {
915 if (d._def.button.toggle) {
916 d[d._def.button.toggle] = !d[d._def.button.toggle];
919 if (d._def.button.onclick) {
920 d._def.button.onclick.call(d);
925 d3.event.preventDefault();
928 function showTouchMenu(obj,pos) {
929 var mdn = mousedown_node;
931 options.push({name:"delete",disabled:(moving_set.length===0),onselect:function() {deleteSelection();}});
932 options.push({name:"cut",disabled:(moving_set.length===0),onselect:function() {copySelection();deleteSelection();}});
933 options.push({name:"copy",disabled:(moving_set.length===0),onselect:function() {copySelection();}});
934 options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard,true);}});
935 options.push({name:"edit",disabled:(moving_set.length != 1),onselect:function() { RED.editor.edit(mdn);}});
936 options.push({name:"select",onselect:function() {selectAll();}});
937 options.push({name:"undo",disabled:(RED.history.depth() === 0),onselect:function() {RED.history.pop();}});
939 RED.touch.radialMenu.show(obj,pos,options);
943 vis.attr("transform","scale("+scaleFactor+")");
944 outer.attr("width", space_width*scaleFactor).attr("height", space_height*scaleFactor);
946 if (mouse_mode != RED.state.JOINING) {
947 // Don't bother redrawing nodes if we're drawing links
949 var node = vis.selectAll(".nodegroup").data(RED.nodes.nodes.filter(function(d) { return d.z == activeWorkspace }),function(d){return d.id});
950 node.exit().remove();
952 var nodeEnter = node.enter().insert("svg:g").attr("class", "node nodegroup");
953 nodeEnter.each(function(d,i) {
954 var node = d3.select(this);
955 node.attr("id",d.id);
956 var l = d._def.label;
957 l = (typeof l === "function" ? l.call(d) : l)||"";
958 d.w = Math.max(node_width,calculateTextWidth(l)+(d._def.inputs>0?7:0) );
959 d.h = Math.max(node_height,(d.outputs||0) * 15);
962 var badge = node.append("svg:g").attr("class","node_badge_group");
963 var badgeRect = badge.append("rect").attr("class","node_badge").attr("rx",5).attr("ry",5).attr("width",40).attr("height",15);
964 badge.append("svg:text").attr("class","node_badge_label").attr("x",35).attr("y",11).attr('text-anchor','end').text(d._def.badge());
965 if (d._def.onbadgeclick) {
966 badgeRect.attr("cursor","pointer")
967 .on("click",function(d) { d._def.onbadgeclick.call(d);d3.event.preventDefault();});
972 var nodeButtonGroup = node.append('svg:g')
973 .attr("transform",function(d) { return "translate("+((d._def.align == "right") ? 94 : -25)+",2)"; })
974 .attr("class",function(d) { return "node_button "+((d._def.align == "right") ? "node_right_button" : "node_left_button"); });
975 nodeButtonGroup.append('rect')
979 .attr("height",node_height-4)
980 .attr("fill","#eee");//function(d) { return d._def.color;})
981 nodeButtonGroup.append('rect')
982 .attr("x",function(d) { return d._def.align == "right"? 10:5})
987 .attr("height",node_height-12)
988 .attr("fill",function(d) { return d._def.color;})
989 .attr("cursor","pointer")
990 .on("mousedown",function(d) {if (!lasso) { d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}})
991 .on("mouseup",function(d) {if (!lasso) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}})
992 .on("mouseover",function(d) {if (!lasso) { d3.select(this).attr("fill-opacity",0.4);}})
993 .on("mouseout",function(d) {if (!lasso) {
995 if (d._def.button.toggle) {
996 op = d[d._def.button.toggle]?1:0.2;
998 d3.select(this).attr("fill-opacity",op);
1000 .on("click",nodeButtonClicked)
1001 .on("touchstart",nodeButtonClicked)
1004 var mainRect = node.append("rect")
1005 .attr("class", "node")
1006 .classed("node_unknown",function(d) { return d.type == "unknown"; })
1009 .attr("fill",function(d) { return d._def.color;})
1010 .on("mouseup",nodeMouseUp)
1011 .on("mousedown",nodeMouseDown)
1012 .on("touchstart",function(d) {
1013 var obj = d3.select(this);
1014 var touch0 = d3.event.touches.item(0);
1015 var pos = [touch0.pageX,touch0.pageY];
1016 startTouchCenter = [touch0.pageX,touch0.pageY];
1017 startTouchDistance = 0;
1018 touchStartTime = setTimeout(function() {
1019 showTouchMenu(obj,pos);
1020 },touchLongPressTimeout);
1021 nodeMouseDown.call(this,d)
1023 .on("touchend", function(d) {
1024 clearTimeout(touchStartTime);
1025 touchStartTime = null;
1026 if (RED.touch.radialMenu.active()) {
1027 d3.event.stopPropagation();
1030 nodeMouseUp.call(this,d);
1032 .on("mouseover",function(d) {
1033 if (mouse_mode === 0) {
1034 var node = d3.select(this);
1035 node.classed("node_hovered",true);
1038 .on("mouseout",function(d) {
1039 var node = d3.select(this);
1040 node.classed("node_hovered",false);
1043 //node.append("rect").attr("class", "node-gradient-top").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-top)").style("pointer-events","none");
1044 //node.append("rect").attr("class", "node-gradient-bottom").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-bottom)").style("pointer-events","none");
1048 var icon_group = node.append("g")
1049 .attr("class","node_icon_group")
1050 .attr("x",0).attr("y",0);
1052 var icon_shade = icon_group.append("rect")
1053 .attr("x",0).attr("y",0)
1054 .attr("class","node_icon_shade")
1056 .attr("stroke","none")
1057 .attr("fill","#000")
1058 .attr("fill-opacity","0.05")
1059 .attr("height",function(d){return Math.min(50,d.h-4);});
1061 var icon = icon_group.append("image")
1062 .attr("xlink:href","icons/"+d._def.icon)
1063 .attr("class","node_icon")
1066 .attr("height","30");
1068 var icon_shade_border = icon_group.append("path")
1069 .attr("d",function(d) { return "M 30 1 l 0 "+(d.h-2)})
1070 .attr("class","node_icon_shade_border")
1071 .attr("stroke-opacity","0.1")
1072 .attr("stroke","#000")
1073 .attr("stroke-width","2");
1075 if ("right" == d._def.align) {
1076 icon_group.attr('class','node_icon_group node_icon_group_'+d._def.align);
1077 icon_shade_border.attr("d",function(d) { return "M 0 1 l 0 "+(d.h-2)})
1078 //icon.attr('class','node_icon node_icon_'+d._def.align);
1079 //icon.attr('class','node_icon_shade node_icon_shade_'+d._def.align);
1080 //icon.attr('class','node_icon_shade_border node_icon_shade_border_'+d._def.align);
1083 //if (d._def.inputs > 0 && d._def.align == null) {
1084 // icon_shade.attr("width",35);
1085 // icon.attr("transform","translate(5,0)");
1086 // icon_shade_border.attr("transform","translate(5,0)");
1088 //if (d._def.outputs > 0 && "right" == d._def.align) {
1089 // icon_shade.attr("width",35); //icon.attr("x",5);
1092 var img = new Image();
1093 img.src = "icons/"+d._def.icon;
1094 img.onload = function() {
1095 icon.attr("width",Math.min(img.width,30));
1096 icon.attr("height",Math.min(img.height,30));
1097 icon.attr("x",15-Math.min(img.width,30)/2);
1098 //if ("right" == d._def.align) {
1099 // icon.attr("x",function(d){return d.w-img.width-1-(d.outputs>0?5:0);});
1100 // icon_shade.attr("x",function(d){return d.w-30});
1101 // icon_shade_border.attr("d",function(d){return "M "+(d.w-30)+" 1 l 0 "+(d.h-2);});
1105 //icon.style("pointer-events","none");
1106 icon_group.style("pointer-events","none");
1108 var text = node.append('svg:text').attr('class','node_label').attr('x', 38).attr('dy', '.35em').attr('text-anchor','start');
1110 text.attr('class','node_label node_label_'+d._def.align);
1111 text.attr('text-anchor','end');
1114 var status = node.append("svg:g").attr("class","node_status_group").style("display","none");
1116 var statusRect = status.append("rect").attr("class","node_status")
1117 .attr("x",6).attr("y",1).attr("width",9).attr("height",9)
1118 .attr("rx",2).attr("ry",2).attr("stroke-width","3");
1120 var statusLabel = status.append("svg:text")
1121 .attr("class","node_status_label")
1122 .attr('x',20).attr('y',9)
1128 'text-anchor':'start'
1131 //node.append("circle").attr({"class":"centerDot","cx":0,"cy":0,"r":5});
1133 if (d._def.inputs > 0) {
1135 node.append("rect").attr("class","port port_input").attr("rx",3).attr("ry",3).attr("x",-5).attr("width",10).attr("height",10)
1136 .on("mousedown",function(d){portMouseDown(d,1,0);})
1137 .on("touchstart",function(d){portMouseDown(d,1,0);})
1138 .on("mouseup",function(d){portMouseUp(d,1,0);} )
1139 .on("touchend",function(d){portMouseUp(d,1,0);} )
1140 .on("mouseover",function(d) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type != 1 ));})
1141 .on("mouseout",function(d) { var port = d3.select(this); port.classed("port_hovered",false);})
1144 //node.append("path").attr("class","node_error").attr("d","M 3,-3 l 10,0 l -5,-8 z");
1145 node.append("image").attr("class","node_error hidden").attr("xlink:href","icons/node-error.png").attr("x",0).attr("y",-6).attr("width",10).attr("height",9);
1146 node.append("image").attr("class","node_changed hidden").attr("xlink:href","icons/node-changed.png").attr("x",12).attr("y",-6).attr("width",10).attr("height",10);
1149 node.each(function(d,i) {
1151 //if (d.x < -50) deleteSelection(); // Delete nodes if dragged back to palette
1153 var l = d._def.label;
1154 l = (typeof l === "function" ? l.call(d) : l)||"";
1155 d.w = Math.max(node_width,calculateTextWidth(l)+(d._def.inputs>0?7:0) );
1156 d.h = Math.max(node_height,(d.outputs||0) * 15);
1158 var thisNode = d3.select(this);
1159 //thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}});
1160 thisNode.attr("transform", function(d) { return "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"; });
1161 thisNode.selectAll(".node")
1162 .attr("width",function(d){return d.w})
1163 .attr("height",function(d){return d.h})
1164 .classed("node_selected",function(d) { return d.selected; })
1165 .classed("node_highlighted",function(d) { return d.highlighted; })
1167 //thisNode.selectAll(".node-gradient-top").attr("width",function(d){return d.w});
1168 //thisNode.selectAll(".node-gradient-bottom").attr("width",function(d){return d.w}).attr("y",function(d){return d.h-30});
1170 thisNode.selectAll(".node_icon_group_right").attr('transform', function(d){return "translate("+(d.w-30)+",0)"});
1171 thisNode.selectAll(".node_label_right").attr('x', function(d){return d.w-38});
1172 //thisNode.selectAll(".node_icon_right").attr("x",function(d){return d.w-d3.select(this).attr("width")-1-(d.outputs>0?5:0);});
1173 //thisNode.selectAll(".node_icon_shade_right").attr("x",function(d){return d.w-30;});
1174 //thisNode.selectAll(".node_icon_shade_border_right").attr("d",function(d){return "M "+(d.w-30)+" 1 l 0 "+(d.h-2)});
1177 var numOutputs = d.outputs;
1178 var y = (d.h/2)-((numOutputs-1)/2)*13;
1179 d.ports = d.ports || d3.range(numOutputs);
1180 d._ports = thisNode.selectAll(".port_output").data(d.ports);
1181 d._ports.enter().append("rect").attr("class","port port_output").attr("rx",3).attr("ry",3).attr("width",10).attr("height",10)
1182 .on("mousedown",(function(){var node = d; return function(d,i){portMouseDown(node,0,i);}})() )
1183 .on("touchstart",(function(){var node = d; return function(d,i){portMouseDown(node,0,i);}})() )
1184 .on("mouseup",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
1185 .on("touchend",(function(){var node = d; return function(d,i){portMouseUp(node,0,i);}})() )
1186 .on("mouseover",function(d,i) { var port = d3.select(this); port.classed("port_hovered",(mouse_mode!=RED.state.JOINING || mousedown_port_type !== 0 ));})
1187 .on("mouseout",function(d,i) { var port = d3.select(this); port.classed("port_hovered",false);});
1188 d._ports.exit().remove();
1190 numOutputs = d.outputs || 1;
1191 y = (d.h/2)-((numOutputs-1)/2)*13;
1193 d._ports.each(function(d,i) {
1194 var port = d3.select(this);
1195 port.attr("y",(y+13*i)-5).attr("x",x);
1198 thisNode.selectAll('text.node_label').text(function(d,i){
1200 if (typeof d._def.label == "function") {
1201 return d._def.label.call(d);
1203 return d._def.label;
1208 .attr('y', function(d){return (d.h/2)-1;})
1209 .attr('class',function(d){
1210 return 'node_label'+
1211 (d._def.align?' node_label_'+d._def.align:'')+
1212 (d._def.labelStyle?' '+(typeof d._def.labelStyle == "function" ? d._def.labelStyle.call(d):d._def.labelStyle):'') ;
1214 thisNode.selectAll(".node_tools").attr("x",function(d){return d.w-35;}).attr("y",function(d){return d.h-20;});
1216 thisNode.selectAll(".node_changed")
1217 .attr("x",function(d){return d.w-10})
1218 .classed("hidden",function(d) { return !d.changed; });
1220 thisNode.selectAll(".node_error")
1221 .attr("x",function(d){return d.w-10-(d.changed?13:0)})
1222 .classed("hidden",function(d) { return d.valid; });
1224 thisNode.selectAll(".port_input").each(function(d,i) {
1225 var port = d3.select(this);
1226 port.attr("y",function(d){return (d.h/2)-5;})
1229 thisNode.selectAll(".node_icon").attr("y",function(d){return (d.h-d3.select(this).attr("height"))/2;});
1230 thisNode.selectAll(".node_icon_shade").attr("height",function(d){return d.h;});
1231 thisNode.selectAll(".node_icon_shade_border").attr("d",function(d){ return "M "+(("right" == d._def.align) ?0:30)+" 1 l 0 "+(d.h-2)});
1234 thisNode.selectAll('.node_right_button').attr("transform",function(d){
1236 if (d._def.button.toggle && !d[d._def.button.toggle]) {
1239 return "translate("+x+",2)";
1241 thisNode.selectAll('.node_right_button rect').attr("fill-opacity",function(d){
1242 if (d._def.button.toggle) {
1243 return d[d._def.button.toggle]?1:0.2;
1248 //thisNode.selectAll('.node_right_button').attr("transform",function(d){return "translate("+(d.w - d._def.button.width.call(d))+","+0+")";}).attr("fill",function(d) {
1249 // return typeof d._def.button.color === "function" ? d._def.button.color.call(d):(d._def.button.color != null ? d._def.button.color : d._def.color)
1252 thisNode.selectAll('.node_badge_group').attr("transform",function(d){return "translate("+(d.w-40)+","+(d.h+3)+")";});
1253 thisNode.selectAll('text.node_badge_label').text(function(d,i) {
1255 if (typeof d._def.badge == "function") {
1256 return d._def.badge.call(d);
1258 return d._def.badge;
1263 if (!showStatus || !d.status) {
1264 thisNode.selectAll('.node_status_group').style("display","none");
1266 thisNode.selectAll('.node_status_group').style("display","inline").attr("transform","translate(3,"+(d.h+3)+")");
1267 var fill = status_colours[d.status.fill]; // Only allow our colours for now
1268 if (d.status.shape == null && fill == null) {
1269 thisNode.selectAll('.node_status').style("display","none");
1272 if (d.status.shape == null || d.status.shape == "dot") {
1278 } else if (d.status.shape == "ring" ){
1285 thisNode.selectAll('.node_status').style(style);
1287 if (d.status.text) {
1288 thisNode.selectAll('.node_status_label').text(d.status.text);
1290 thisNode.selectAll('.node_status_label').text("");
1299 var link = vis.selectAll(".link").data(RED.nodes.links.filter(function(d) { return d.source.z == activeWorkspace && d.target.z == activeWorkspace }),function(d) { return d.source.id+":"+d.sourcePort+":"+d.target.id;});
1301 var linkEnter = link.enter().insert("g",".node").attr("class","link");
1303 linkEnter.each(function(d,i) {
1304 var l = d3.select(this);
1305 l.append("svg:path").attr("class","link_background link_path")
1306 .on("mousedown",function(d) {
1309 selected_link = mousedown_link;
1312 d3.event.stopPropagation();
1314 .on("touchstart",function(d) {
1317 selected_link = mousedown_link;
1320 d3.event.stopPropagation();
1322 l.append("svg:path").attr("class","link_outline link_path");
1323 l.append("svg:path").attr("class","link_line link_path");
1326 link.exit().remove();
1328 var links = vis.selectAll(".link_path")
1329 links.attr("d",function(d){
1330 var numOutputs = d.source.outputs || 1;
1331 var sourcePort = d.sourcePort || 0;
1332 var y = -((numOutputs-1)/2)*13 +13*sourcePort;
1334 var dy = d.target.y-(d.source.y+y);
1335 var dx = (d.target.x-d.target.w/2)-(d.source.x+d.source.w/2);
1336 var delta = Math.sqrt(dy*dy+dx*dx);
1337 var scale = lineCurveScale;
1339 if (delta < node_width) {
1340 scale = 0.75-0.75*((node_width-delta)/node_width);
1344 scale += 2*(Math.min(5*node_width,Math.abs(dx))/(5*node_width));
1345 if (Math.abs(dy) < 3*node_height) {
1346 scaleY = ((dy>0)?0.5:-0.5)*(((3*node_height)-Math.abs(dy))/(3*node_height))*(Math.min(node_width,Math.abs(dx))/(node_width)) ;
1350 d.x1 = d.source.x+d.source.w/2;
1351 d.y1 = d.source.y+y;
1352 d.x2 = d.target.x-d.target.w/2;
1355 return "M "+(d.source.x+d.source.w/2)+" "+(d.source.y+y)+
1356 " C "+(d.source.x+d.source.w/2+scale*node_width)+" "+(d.source.y+y+scaleY*node_height)+" "+
1357 (d.target.x-d.target.w/2-scale*node_width)+" "+(d.target.y-scaleY*node_height)+" "+
1358 (d.target.x-d.target.w/2)+" "+d.target.y;
1361 link.classed("link_selected", function(d) { return d === selected_link || d.selected; });
1362 link.classed("link_unknown",function(d) { return d.target.type == "unknown" || d.source.type == "unknown"});
1365 d3.event.preventDefault();
1369 RED.keyboard.add(/* z */ 90,{ctrl:true},function(){RED.history.pop();});
1370 RED.keyboard.add(/* a */ 65,{ctrl:true},function(){selectAll();d3.event.preventDefault();});
1371 RED.keyboard.add(/* = */ 187,{ctrl:true},function(){zoomIn();d3.event.preventDefault();});
1372 RED.keyboard.add(/* - */ 189,{ctrl:true},function(){zoomOut();d3.event.preventDefault();});
1373 RED.keyboard.add(/* 0 */ 48,{ctrl:true},function(){zoomZero();d3.event.preventDefault();});
1374 RED.keyboard.add(/* v */ 86,{ctrl:true},function(){importNodes(clipboard);d3.event.preventDefault();});
1375 RED.keyboard.add(/* e */ 69,{ctrl:true},function(){showExportNodesDialog();d3.event.preventDefault();});
1376 RED.keyboard.add(/* i */ 73,{ctrl:true},function(){showImportNodesDialog();d3.event.preventDefault();});
1378 // TODO: 'dirty' should be a property of RED.nodes - with an event callback for ui hooks
1379 function setDirty(d) {
1382 $("#btn-deploy").removeClass("disabled");
1384 $("#btn-deploy").addClass("disabled");
1389 * Imports a new collection of nodes from a JSON String.
1390 * - all get new IDs assigned
1392 * - attached to mouse for placing - 'IMPORT_DRAGGING'
1394 function importNodes(newNodesStr,touchImport) {
1396 var result = RED.nodes.import(newNodesStr,true);
1398 var new_nodes = result[0];
1399 var new_links = result[1];
1400 var new_workspaces = result[2];
1402 var new_ms = new_nodes.filter(function(n) { return n.z == activeWorkspace }).map(function(n) { return {n:n};});
1403 var new_node_ids = new_nodes.map(function(n){ return n.id; });
1405 // TODO: pick a more sensible root node
1406 if (new_ms.length > 0) {
1407 var root_node = new_ms[0].n;
1408 var dx = root_node.x;
1409 var dy = root_node.y;
1411 if (mouse_position == null) {
1412 mouse_position = [0,0];
1420 for (i=0;i<new_ms.length;i++) {
1422 node.n.selected = true;
1423 node.n.changed = true;
1424 node.n.x -= dx - mouse_position[0];
1425 node.n.y -= dy - mouse_position[1];
1426 node.dx = node.n.x - mouse_position[0];
1427 node.dy = node.n.y - mouse_position[1];
1428 minX = Math.min(node.n.x-node_width/2-5,minX);
1429 minY = Math.min(node.n.y-node_height/2-5,minY);
1431 for (i=0;i<new_ms.length;i++) {
1439 mouse_mode = RED.state.IMPORT_DRAGGING;
1442 RED.keyboard.add(/* ESCAPE */ 27,function(){
1443 RED.keyboard.remove(/* ESCAPE */ 27);
1449 moving_set = new_ms;
1452 RED.history.push({t:'add',nodes:new_node_ids,links:new_links,workspaces:new_workspaces,dirty:RED.view.dirty()});
1458 console.log(error.stack);
1459 RED.notify("<strong>Error</strong>: "+error,"error");
1463 function showExportNodesDialog() {
1464 mouse_mode = RED.state.EXPORT;
1465 var nns = RED.nodes.createExportableNodeSet(moving_set);
1466 $("#dialog-form").html($("script[data-template-name='export-clipboard-dialog']").html());
1467 $("#node-input-export").val(JSON.stringify(nns));
1468 $("#node-input-export").focus(function() {
1469 var textarea = $(this);
1471 textarea.mouseup(function() {
1472 textarea.unbind("mouseup");
1476 $( "#dialog" ).dialog("option","title","Export nodes to clipboard").dialog( "open" );
1477 $("#node-input-export").focus();
1480 function showExportNodesLibraryDialog() {
1481 mouse_mode = RED.state.EXPORT;
1482 var nns = RED.nodes.createExportableNodeSet(moving_set);
1483 $("#dialog-form").html($("script[data-template-name='export-library-dialog']").html());
1484 $("#node-input-filename").attr('nodes',JSON.stringify(nns));
1485 $( "#dialog" ).dialog("option","title","Export nodes to library").dialog( "open" );
1488 function showImportNodesDialog() {
1489 mouse_mode = RED.state.IMPORT;
1490 $("#dialog-form").html($("script[data-template-name='import-dialog']").html());
1491 $("#node-input-import").val("");
1492 $( "#dialog" ).dialog("option","title","Import nodes").dialog( "open" );
1495 function showRenameWorkspaceDialog(id) {
1496 var ws = RED.nodes.workspace(id);
1497 $( "#node-dialog-rename-workspace" ).dialog("option","workspace",ws);
1499 if (workspace_tabs.count() == 1) {
1500 $( "#node-dialog-rename-workspace").next().find(".leftButton")
1501 .prop('disabled',true)
1502 .addClass("ui-state-disabled");
1504 $( "#node-dialog-rename-workspace").next().find(".leftButton")
1505 .prop('disabled',false)
1506 .removeClass("ui-state-disabled");
1509 $( "#node-input-workspace-name" ).val(ws.label);
1510 $( "#node-dialog-rename-workspace" ).dialog("open");
1513 $("#node-dialog-rename-workspace form" ).submit(function(e) { e.preventDefault();});
1514 $( "#node-dialog-rename-workspace" ).dialog({
1518 title: "Rename sheet",
1521 class: 'leftButton',
1524 var workspace = $(this).dialog('option','workspace');
1525 $( this ).dialog( "close" );
1526 deleteWorkspace(workspace.id);
1532 var workspace = $(this).dialog('option','workspace');
1533 var label = $( "#node-input-workspace-name" ).val();
1534 if (workspace.label != label) {
1535 workspace.label = label;
1536 var link = $("#workspace-tabs a[href='#"+workspace.id+"']");
1537 link.attr("title",label);
1539 RED.view.dirty(true);
1541 $( this ).dialog( "close" );
1547 $( this ).dialog( "close" );
1552 RED.keyboard.disable();
1554 close: function(e) {
1555 RED.keyboard.enable();
1558 $( "#node-dialog-delete-workspace" ).dialog({
1562 title: "Confirm delete",
1567 var workspace = $(this).dialog('option','workspace');
1568 RED.view.removeWorkspace(workspace);
1569 var historyEvent = RED.nodes.removeWorkspace(workspace.id);
1570 historyEvent.t = 'delete';
1571 historyEvent.dirty = dirty;
1572 historyEvent.workspaces = [workspace];
1573 RED.history.push(historyEvent);
1574 RED.view.dirty(true);
1575 $( this ).dialog( "close" );
1581 $( this ).dialog( "close" );
1586 RED.keyboard.disable();
1588 close: function(e) {
1589 RED.keyboard.enable();
1595 state:function(state) {
1596 if (state == null) {
1602 addWorkspace: function(ws) {
1603 workspace_tabs.addTab(ws);
1604 workspace_tabs.resize();
1606 removeWorkspace: function(ws) {
1607 workspace_tabs.removeTab(ws.id);
1609 getWorkspace: function() {
1610 return activeWorkspace;
1612 showWorkspace: function(id) {
1613 workspace_tabs.activateTab(id);
1616 dirty: function(d) {
1623 importNodes: importNodes,
1624 resize: function() {
1625 workspace_tabs.resize();
1627 status: function(s) {
1629 RED.nodes.eachNode(function(n) { n.dirty = true;});
1630 //TODO: subscribe/unsubscribe here
1634 //TODO: should these move to an import/export module?
1635 showImportNodesDialog: showImportNodesDialog,
1636 showExportNodesDialog: showExportNodesDialog,
1637 showExportNodesLibraryDialog: showExportNodesLibraryDialog