diff --git a/Gruntfile.js b/Gruntfile.js index 261f0c72b..a5a43d053 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -199,7 +199,8 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js", "packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectUserSettings.js", "packages/node_modules/@node-red/editor-client/src/js/ui/projects/tab-versionControl.js", - "packages/node_modules/@node-red/editor-client/src/js/ui/touch/radialMenu.js" + "packages/node_modules/@node-red/editor-client/src/js/ui/touch/radialMenu.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/tour/*.js" ], dest: "packages/node_modules/@node-red/editor-client/public/red/red.js" }, @@ -326,6 +327,12 @@ module.exports = function(grunt) { ], tasks: ['jsonlint:keymaps','copy:build'] }, + tours: { + files: [ + 'packages/node_modules/@node-red/editor-client/src/tours/**/*.js' + ], + tasks: ['copy:build'] + }, misc: { files: [ 'CHANGELOG.md' @@ -423,6 +430,12 @@ module.exports = function(grunt) { src: '**', expand: true, dest: 'packages/node_modules/@node-red/editor-client/public/vendor/ace/' + }, + { + cwd: 'packages/node_modules/@node-red/editor-client/src/tours', + src: '**', + expand: true, + dest: 'packages/node_modules/@node-red/editor-client/public/red/tours/' } ] } diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 2a1e16f1d..95c803216 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -112,6 +112,7 @@ "editPalette":"Manage palette", "other": "Other", "showTips": "Show tips", + "showWelcomeTours": "Show guided tours for new versions", "help": "Node-RED website", "projects": "Projects", "projects-new": "New", @@ -1132,6 +1133,10 @@ "preview": "UI Preview", "defaultValue": "Default value" }, + "tourGuide": { + "start": "Start", + "next": "Next" + }, "languages" : { "de": "German", "en-US": "English", diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index ad68ea612..d9ef01021 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -535,19 +535,18 @@ var RED = (function() { setTimeout(function() { loader.end(); + checkFirstRun(); },100); } - function showAbout() { - $.get('red/about', function(data) { - // data will be strictly markdown. Any HTML should be escaped. - data = RED.utils.sanitize(data); - var aboutHeader = '
'+ - ''+ - '
'; - - RED.sidebar.help.set(aboutHeader+RED.utils.renderMarkdown(data)); - }); + function checkFirstRun() { + if (RED.settings.theme("tours") === false) { + return; + } + if (!RED.settings.get("editor.view.view-show-welcome-tours", true)) { + return; + } + RED.actions.invoke("core:show-welcome-tour", RED.settings.get("editor.tours.welcome")); } function buildMainMenu() { @@ -696,9 +695,6 @@ var RED = (function() { $("#red-ui-main-container").show(); - - RED.actions.add("core:show-about", showAbout); - loadPluginList(); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/settings.js b/packages/node_modules/@node-red/editor-client/src/js/settings.js index 467c5b569..1c6513c43 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/settings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/settings.js @@ -19,7 +19,6 @@ RED.settings = (function () { var loadedSettings = {}; var userSettings = {}; - var settingsDirty = false; var pendingSave; var hasLocalStorage = function () { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js index 8d7553069..c40570626 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js @@ -17,20 +17,12 @@ RED.popover = (function() { var deltaSizes = { "default": { - top: 10, - topTop: 30, - leftRight: 17, - leftLeft: 25, - leftBottom: 8, - leftTop: 11 + x: 12, + y: 12 }, "small": { - top: 6, - topTop: 20, - leftRight: 8, - leftLeft: 26, - leftBottom: 8, - leftTop: 9 + x:8, + y:8 } } function createPopover(options) { @@ -41,7 +33,9 @@ RED.popover = (function() { var delay = options.delay || { show: 750, hide: 50 }; var autoClose = options.autoClose; var width = options.width||"auto"; + var maxWidth = options.maxWidth; var size = options.size||"default"; + var popupOffset = options.offset || 0; if (!deltaSizes[size]) { throw new Error("Invalid RED.popover size value:",size); } @@ -49,6 +43,8 @@ RED.popover = (function() { var timer = null; var active; var div; + var contentDiv; + var currentStyle; var openPopup = function(instant) { if (active) { @@ -58,6 +54,10 @@ RED.popover = (function() { return; } div = $('
'); + if (options.class) { + div.addClass(options.class); + } + contentDiv = $('
').appendTo(div); if (size !== "default") { div.addClass("red-ui-popover-size-"+size); } @@ -67,71 +67,23 @@ RED.popover = (function() { return; } if (typeof result === 'string') { - div.text(result); + contentDiv.text(result); } else { - div.append(result); + contentDiv.append(result); } } else { - div.html(content); - } - if (width !== "auto") { - div.width(width); + contentDiv.html(content); } div.appendTo("body"); - var targetPos = target.offset(); - var targetWidth = target.outerWidth(); - var targetHeight = target.outerHeight(); - var divHeight = div.height(); - var divWidth = div.width(); - var paddingRight = 10; + movePopup({target,direction,width,maxWidth}); - var viewportTop = $(window).scrollTop(); - var viewportLeft = $(window).scrollLeft(); - var viewportBottom = viewportTop + $(window).height(); - var viewportRight = viewportLeft + $(window).width(); - var top = 0; - var left = 0; - var d = direction; - if (d === 'right') { - top = targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top; - left = targetPos.left+targetWidth+deltaSizes[size].leftRight; - } else if (d === 'left') { - top = targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top; - left = targetPos.left-deltaSizes[size].leftLeft-divWidth; - } else if (d === 'bottom') { - top = targetPos.top+targetHeight+deltaSizes[size].top; - left = targetPos.left+targetWidth/2-divWidth/2 - deltaSizes[size].leftBottom; - if (left < 0) { - d = "right"; - top = targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top; - left = targetPos.left+targetWidth+deltaSizes[size].leftRight; - } else if (left+divWidth+paddingRight > viewportRight) { - d = "left"; - top = targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top; - left = targetPos.left-deltaSizes[size].leftLeft-divWidth; - if (top+divHeight+targetHeight/2 + 5 > viewportBottom) { - top -= (top+divHeight+targetHeight/2 - viewportBottom + 5) - } - } else if (top+divHeight > viewportBottom) { - d = 'top'; - top = targetPos.top-deltaSizes[size].topTop-divHeight; - left = targetPos.left+targetWidth/2-divWidth/2 - deltaSizes[size].leftTop; - } - } else if (d === 'top') { - top = targetPos.top-deltaSizes[size].topTop-divHeight; - left = targetPos.left+targetWidth/2-divWidth/2 - deltaSizes[size].leftTop; - if (top < 0) { - d = 'bottom'; - top = targetPos.top+targetHeight+deltaSizes[size].top; - left = targetPos.left+targetWidth/2-divWidth/2 - deltaSizes[size].leftBottom; - } - } - div.addClass('red-ui-popover-'+d).css({top: top, left: left}); if (existingPopover) { existingPopover.close(true); } - target.data("red-ui-popover",res) + if (options.trigger !== 'manual') { + target.data("red-ui-popover",res) + } if (options.tooltip) { div.on("mousedown", function(evt) { closePopup(true); @@ -161,6 +113,102 @@ RED.popover = (function() { } } } + var movePopup = function(options) { + target = options.target || target; + direction = options.direction || direction || "right"; + popupOffset = options.offset || popupOffset; + var transition = options.transition; + + var width = options.width||"auto"; + div.width(width); + if (options.maxWidth) { + div.css("max-width",options.maxWidth) + } + + var targetPos = target[0].getBoundingClientRect(); + var targetHeight = targetPos.height; + var targetWidth = targetPos.width; + + var divHeight = div.outerHeight(); + var divWidth = div.outerWidth(); + var paddingRight = 10; + + var viewportTop = $(window).scrollTop(); + var viewportLeft = $(window).scrollLeft(); + var viewportBottom = viewportTop + $(window).height(); + var viewportRight = viewportLeft + $(window).width(); + var top = 0; + var left = 0; + if (direction === 'right') { + top = targetPos.top+targetHeight/2-divHeight/2; + left = targetPos.left+targetWidth+deltaSizes[size].x+popupOffset; + } else if (direction === 'left') { + top = targetPos.top+targetHeight/2-divHeight/2; + left = targetPos.left-deltaSizes[size].x-divWidth-popupOffset; + } else if (direction === 'bottom') { + top = targetPos.top+targetHeight+deltaSizes[size].y+popupOffset; + left = targetPos.left+targetWidth/2-divWidth/2; + if (left < 0) { + direction = "right"; + top = targetPos.top+targetHeight/2-divHeight/2; + left = targetPos.left+targetWidth+deltaSizes[size].x+popupOffset; + } else if (left+divWidth+paddingRight > viewportRight) { + direction = "left"; + top = targetPos.top+targetHeight/2-divHeight/2; + left = targetPos.left-deltaSizes[size].x-divWidth-popupOffset; + if (top+divHeight+targetHeight/2 + 5 > viewportBottom) { + top -= (top+divHeight+targetHeight/2 - viewportBottom + 5) + } + } else if (top+divHeight > viewportBottom) { + direction = 'top'; + top = targetPos.top-deltaSizes[size].y-divHeight-popupOffset; + left = targetPos.left+targetWidth/2-divWidth/2; + } + } else if (direction === 'top') { + top = targetPos.top-deltaSizes[size].y-divHeight-popupOffset; + left = targetPos.left+targetWidth/2-divWidth/2; + if (top < 0) { + direction = 'bottom'; + top = targetPos.top+targetHeight+deltaSizes[size].y+popupOffset; + left = targetPos.left+targetWidth/2-divWidth/2; + } + } else if (/inset/.test(direction)) { + top = targetPos.top + targetHeight/2 - divHeight/2; + left = targetPos.left + targetWidth/2 - divWidth/2; + + if (/bottom/.test(direction)) { + top = targetPos.top + targetHeight - divHeight-popupOffset; + } + if (/top/.test(direction)) { + top = targetPos.top+popupOffset; + } + if (/left/.test(direction)) { + left = targetPos.left+popupOffset; + } + if (/right/.test(direction)) { + left = targetPos.left + targetWidth - divWidth-popupOffset; + } + } + if (currentStyle) { + div.removeClass(currentStyle); + } + if (transition) { + div.css({ + "transition": "0.6s ease", + "transition-property": "top,left,right,bottom" + }) + } + currentStyle = 'red-ui-popover-'+direction; + div.addClass(currentStyle).css({top: top, left: left}); + if (transition) { + setTimeout(function() { + div.css({ + "transition": "none" + }); + },600); + } + + } var closePopup = function(instant) { $(document).off('mousedown.red-ui-popover'); if (!active) { @@ -236,8 +284,10 @@ RED.popover = (function() { },autoClose); } var res = { + get element() { return div }, setContent: function(_content) { content = _content; + return res; }, open: function (instant) { @@ -249,6 +299,10 @@ RED.popover = (function() { active = false; closePopup(instant); return res; + }, + move: function(options) { + movePopup(options); + return } } return res; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 27be8db63..b6c11e5f3 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -1409,7 +1409,7 @@ RED.editor = (function() { RED.tray.show(trayOptions); } - function showEditSubflowDialog(subflow) { + function showEditSubflowDialog(subflow, defaultTab) { if (buildingEditDialog) { return } buildingEditDialog = true; var editing_node = subflow; @@ -1549,7 +1549,7 @@ RED.editor = (function() { 'editor-tab-appearance' ]; - prepareEditDialog(trayBody, nodeEditPanes, subflow, subflow._def, "node-input", null, function(_activeEditPanes) { + prepareEditDialog(trayBody, nodeEditPanes, subflow, subflow._def, "node-input", defaultTab, function(_activeEditPanes) { activeEditPanes = _activeEditPanes; $("#subflow-input-name").val(subflow.name); RED.text.bidi.prepareInput($("#subflow-input-name")); @@ -1579,7 +1579,7 @@ RED.editor = (function() { RED.tray.show(trayOptions); } - function showEditGroupDialog(group) { + function showEditGroupDialog(group, defaultTab) { if (buildingEditDialog) { return } buildingEditDialog = true; var editing_node = group; @@ -1661,7 +1661,7 @@ RED.editor = (function() { 'editor-tab-envProperties', 'editor-tab-description' ]; - prepareEditDialog(trayBody, nodeEditPanes, group,group._def,"node-input", null, function(_activeEditPanes) { + prepareEditDialog(trayBody, nodeEditPanes, group,group._def,"node-input", defaultTab, function(_activeEditPanes) { activeEditPanes = _activeEditPanes; trayBody.i18n(); buildingEditDialog = false; @@ -1692,7 +1692,7 @@ RED.editor = (function() { RED.tray.show(trayOptions); } - function showEditFlowDialog(workspace) { + function showEditFlowDialog(workspace, defaultTab) { if (buildingEditDialog) { return } buildingEditDialog = true; var activeEditPanes = []; @@ -1796,8 +1796,7 @@ RED.editor = (function() { disabledIcon: "fa-ban", invertState: true }) - - prepareEditDialog(trayBody, nodeEditPanes, workspace, {}, "node-input", null, function(_activeEditPanes) { + prepareEditDialog(trayBody, nodeEditPanes, workspace, {}, "node-input", defaultTab, function(_activeEditPanes) { activeEditPanes = _activeEditPanes; trayBody.i18n(); trayFooter.i18n(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js index 6add6abe9..ed891c114 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-help.js @@ -25,7 +25,6 @@ RED.sidebar.help = (function() { var tocPanel; var helpIndex = {}; - function resizeStack() { var h = $(content).parent().height() - toolbar.outerHeight(); panels.resize(h) @@ -93,9 +92,28 @@ RED.sidebar.help = (function() { $(''+RED._("sidebar.help.noHelp")+'').appendTo(helpSection); treeList = $("
").css({width: "100%"}).appendTo(tocPanel).treeList({data: []}) + var pendingContentLoad; treeList.on('treelistselect', function(e,item) { + pendingContentLoad = item; if (item.nodeType) { - showHelp(item.nodeType); + showNodeTypeHelp(item.nodeType); + } else if (item.content) { + helpSection.empty(); + if (typeof item.content === "string") { + setInfoText(item.label, item.content); + } else if (typeof item.content === "function") { + if (item.content.length === 0) { + setInfoText(item.label, item.content()); + } else { + setInfoText(item.label, '
',helpSection) + item.content(function(content) { + if (pendingContentLoad === item) { + helpSection.empty(); + setInfoText(item.label, content); + } + }) + } + } } }) @@ -174,21 +192,28 @@ RED.sidebar.help = (function() { var moduleNames = Object.keys(modules); moduleNames.sort(); - var helpData = [{ + var nodeHelp = { label: RED._("sidebar.help.nodeHelp"), children: [], expanded: true - }] - + } + var helpData = [ + { + id: 'changelog', + label: "Node-RED v"+RED.settings.version, + content: getChangelog + }, + nodeHelp + ] var subflows = RED.nodes.registry.getNodeTypes().filter(function(t) {return /subflow/.test(t)}); if (subflows.length > 0) { - helpData[0].children.push({ + nodeHelp.children.push({ label: RED._("menu.label.subflows"), children: [] }) subflows.forEach(function(nodeType) { var sf = RED.nodes.getType(nodeType); - helpData[0].children[0].children.push({ + nodeHelp.children[0].children.push({ id:"node-type:"+nodeType, nodeType: nodeType, subflowLabel: sf.label().toLowerCase(), @@ -218,7 +243,7 @@ RED.sidebar.help = (function() { nodeTypes.sort(function(A,B) { return A.nodeType.localeCompare(B.nodeType) }) - helpData[0].children.push({ + nodeHelp.children.push({ id: moduleName, icon: "fa fa-cube", label: moduleName, @@ -244,7 +269,7 @@ RED.sidebar.help = (function() { return div; } - function showHelp(nodeType) { + function showNodeTypeHelp(nodeType) { helpSection.empty(); var helpText; var title; @@ -265,7 +290,7 @@ RED.sidebar.help = (function() { } } } - setInfoText(title, helpText, helpSection); + setInfoText(title, helpText); var ratio = panels.ratio(); if (ratio > 0.7) { @@ -282,7 +307,7 @@ RED.sidebar.help = (function() { } if (type) { // hideTOC(); - showHelp(type); + showNodeTypeHelp(type); } resizeStack(); } @@ -298,11 +323,12 @@ RED.sidebar.help = (function() { return el; } - function setInfoText(title, infoText,target) { + function setInfoText(title, infoText) { + helpSection.empty(); if (title) { - $("

",{class:"red-ui-help-title"}).text(title).appendTo(target); + $("

",{class:"red-ui-help-title"}).text(title).appendTo(helpSection); } - var info = addTargetToExternalLinks($('
'+infoText+'
')).appendTo(target); + var info = addTargetToExternalLinks($('
'+infoText+'
')).appendTo(helpSection); info.find(".red-ui-text-bidi-aware").contents().filter(function() { return this.nodeType === 3 && this.textContent.trim() !== "" }).wrap( "" ); var foldingHeader = "H3"; info.find(foldingHeader).wrapInner('') @@ -316,12 +342,12 @@ RED.sidebar.help = (function() { } $(this).toggleClass('expanded',!isExpanded); }) - target.parent().scrollTop(0); + helpSection.parent().scrollTop(0); } function set(html,title) { $(helpSection).empty(); - setInfoText(title,html,helpSection); + setInfoText(title,html); hideTOC(); show(); } @@ -336,13 +362,83 @@ RED.sidebar.help = (function() { if (node.type === "subflow" && node.direction) { // ignore subflow virtual ports } else if (node.type !== 'group'){ - showHelp(node.type); + showNodeTypeHelp(node.type); } } } } RED.events.on("view:selection-changed",refreshSelection); + function getChangelog(done) { + $.get('red/about', function(data) { + // data will be strictly markdown. Any HTML should be escaped. + data = RED.utils.sanitize(data); + RED.tourGuide.load("./tours/welcome.js", function(err, tour) { + var tourHeader = '
'; + if (tour) { + var currentVersionParts = RED.settings.version.split("."); + var tourVersionParts = tour.version.split("."); + if (tourVersionParts[0] === currentVersionParts[0] && tourVersionParts[1] === currentVersionParts[1]) { + tourHeader = '
' + } + } + var aboutHeader = '
'+tourHeader+'
' + done(aboutHeader+RED.utils.renderMarkdown(data)) + }); + + }); + } + function showAbout() { + treeList.treeList("show","changelog") + treeList.treeList("select","changelog"); + show(); + } + function showWelcomeTour(lastSeenVersion) { + RED.tourGuide.load("./tours/welcome.js", function(err, tour) { + if (err) { + console.warn("Failed to load welcome tour",err); + return; + } + var currentVersionParts = RED.settings.version.split("."); + var tourVersionParts = tour.version.split("."); + + // Only display the tour if its MAJ.MIN versions the current version + // This means if we update MAJ/MIN without updating the tour, the old tour won't get shown + if (tourVersionParts[0] !== currentVersionParts[0] || tourVersionParts[1] !== currentVersionParts[1]) { + return; + } + + if (lastSeenVersion) { + // Previously displayed a welcome tour. + if (lastSeenVersion === RED.settings.version) { + // Exact match - don't show the tour + return; + } + var lastSeenParts = lastSeenVersion.split("."); + if (currentVersionParts[0] < lastSeenParts[0] || (currentVersionParts[0] === lastSeenParts[0] && currentVersionParts[1] < lastSeenParts[1])) { + // Running an *older* version than last displayed tour. + return; + } + if (currentVersionParts[0] === lastSeenParts[0] && currentVersionParts[1] === lastSeenParts[1]) { + if (lastSeenParts.length === 3 && currentVersionParts.length === 3) { + // Matching non-beta MAJ.MIN - don't repeat tour + return; + } + if (currentVersionParts.length === 4 && (lastSeenParts.length === 3 || currentVersionParts[3] < lastSeenParts[3])) { + // Running an *older* beta than last displayed tour. + return + } + } + } + RED.tourGuide.run("./tours/welcome.js", function(err) { + RED.settings.set("editor.tours.welcome", RED.settings.version) + }) + }) + + } + RED.actions.add("core:show-about", showAbout); + RED.actions.add("core:show-welcome-tour", showWelcomeTour); + return { init: init, show: show, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tour/tourGuide.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tour/tourGuide.js new file mode 100644 index 000000000..e6933f01c --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tour/tourGuide.js @@ -0,0 +1,410 @@ +RED.tourGuide = (function() { + var activeListeners = []; + var shade; + var focus; + var popover; + var stepContent; + var targetElement; + var fullscreen; + + var tourCache = {}; + + function run(tourPath, done) { + done = done || function(err) { + if (err) { + console.error(err); + } + }; + loadTour(tourPath, function(err, tour) { + if (err) { + console.warn("Error loading tour:",err); + return; + } + runTour(tour, done); + }) + + } + + function loadTour(tourPath, done) { + if (tourCache[tourPath]) { + done(null, tourCache[tourPath]); + } else { + /* jshint ignore:start */ + // jshint<2.13 doesn't support dynamic imports. Once grunt-contrib-jshint + // has been updated with the new jshint, we can stop ignoring this block + import(tourPath).then(function(module) { + tourCache[tourPath] = module.default; + done(null, tourCache[tourPath]); + }).catch(function(err) { + done(err); + }) + /* jshint ignore:end */ + } + } + + function repositionFocus() { + if (targetElement) { + var pos = targetElement[0].getBoundingClientRect(); + var dimension = Math.max(50, Math.max(pos.width,pos.height)*1.5); + if (!fullscreen) { + focus.css({ + left: (pos.left+pos.width/2)+"px", + top: (pos.top+pos.height/2)+"px", + width: (2*dimension)+"px", + height: (2*dimension)+"px" + }) + var flush = focus[0].offsetHeight; // Flush CSS changes + focus.addClass("transition"); + focus.css({ + width: dimension+"px", + height: dimension+"px" + }) + } else { + focus.css({ + left: ($(window).width()/2)+"px", + top: ($(window).height()/2)+"px", + width: "0px", + height: "0px" + }) + } + if (popover) { + popover.move({ + target: targetElement, + }) + } + } + } + function runTour(tour, done) { + + shade = $('
').appendTo(document.body); + focus = $('
').appendTo(shade); + + // var resizeTimer; + // + $(window).on("resize.red-ui-tourGuide", function() { + repositionFocus(); + }) + + + + var i = 0; + var state = { + index: 0, + count: tour.steps.length + }; + + function endTour(err) { + $(window).off("resize.red-ui-tourGuide"); + $(document).off('keydown.red-ui-tourGuide'); + if (popover) { + popover.close(); + } + stepContent = null; + popover = null; + shade.remove(); + shade = null; + done(err); + } + function runStep(carryOn) { + if (carryOn === false) { + endTour(false); + return; + } + if (i === tour.steps.length) { + endTour(); + return + } + state.index = i; + // console.log("TOUR STEP",i+1,"OF",tour.steps.length) + try { + runTourStep(tour.steps[i++], state, runStep) + } catch(err) { + endTour(err); + return; + } + } + runStep(); + } + + function clearListeners() { + activeListeners.forEach(function(listener) { + if (listener.type === "dom-event") { + listener.target[0].removeEventListener(listener.event,listener.listener,listener.opts); + } else if (listener.type === "nr-event") { + RED.events.off(listener.event, listener.listener) + } + }) + activeListeners = []; + } + + function prepareStep(step, state, done) { + if (step.prepare) { + if (step.prepare.length === 0) { + step.prepare.call(state); + } else { + step.prepare.call(state, done) + return; + } + } + done(); + } + function completeStep(step, state, done) { + function finish() { + clearListeners(); + setTimeout(function() { + done(); + },0) + } + if (step.complete) { + if (step.complete.length === 0) { + step.complete.call(state); + } else { + step.complete.call(state, finish) + return; + } + } + finish(); + + } + function getLocaleText(property) { + if (typeof property === 'string') { + return property; + } + var currentLang = RED.i18n.lang() || 'en-US'; + var availableLangs = Object.keys(property); + return property[currentLang]||property['en-US']||property[availableLangs[0]] + + } + function runTourStep(step, state, done) { + shade.fadeIn(); + prepareStep(step, state, function() { + var zIndex; + var direction = step.direction || "bottom"; + fullscreen = false; + + if (typeof step.element === "string") { + targetElement = $(step.element) + } else if (typeof step.element === "function") { + targetElement = step.element.call(state); + } else if (!step.element) { + targetElement = $(".red-ui-editor") + fullscreen = true; + direction = "inset"; + } else { + targetElement = step.element; + } + + if (targetElement.length === 0) { + targetElement = null; + shade.hide(); + throw new Error("Element not found") + } + if ($(window).width() < 400) { + targetElement = $(".red-ui-editor"); + fullscreen = true; + direction = "inset"; + } + + zIndex = targetElement.css("z-index"); + if (!fullscreen) { + targetElement.css("z-index",2002); + } + repositionFocus(); + focus.toggleClass("disableInteraction", step.interactive === false) + + if (!stepContent) { + stepContent = $('
'); + } else { + stepContent.empty(); + } + $('').appendTo(stepContent).click(function(evt) { + evt.preventDefault(); + completeStep(step, state, function() { + done(false); + }); + }) + + var stepDescription = $('
').appendTo(stepContent); + if (step.titleIcon) { + $('

').appendTo(stepDescription); + } + if (step.title) { + $('

').text(getLocaleText(step.title)).appendTo(stepDescription); + } + $('
').css("text-align","left").html(getLocaleText(step.description)).appendTo(stepDescription); + + var stepToolbar = $('
',{class:"red-ui-tourGuide-toolbar"}).appendTo(stepContent); + + // var breadcrumbs = $('
',{class:"red-ui-tourGuide-breadcrumbs"}).appendTo(stepToolbar); + // var bcStart = Math.max(0,state.index - 3); + // var bcEnd = Math.min(state.count, bcStart + 7); + // if (bcEnd === state.count) { + // bcStart = Math.max(0,bcEnd - 7); + // } + // for (var i = bcStart; i < bcEnd; i++) { + // var bullet = $('').addClass(i===state.index ? "fa-circle":"fa-circle-o").appendTo(breadcrumbs); + // if (i === bcStart) { + // if (i > 1) { + // bullet.css("font-size", "3px"); + // } else if (i === 1) { + // bullet.css("font-size", "4px"); + // } + // } else if (i === bcStart + 1) { + // if (i > 2) { + // bullet.css("font-size", "4px"); + // } + // } + // if (i === bcEnd - 1) { + // if (i < state.count - 2) { + // bullet.css("font-size", "3px"); + // } else if (i === state.count - 2) { + // bullet.css("font-size", "4px"); + // } + // } else if (i === bcEnd - 2) { + // if (i < state.count - 3) { + // bullet.css("font-size", "4px"); + // } + // } + // // if (i === bcEnd - 1) { + // // if (i < state.count - 2) { + // // bullet.css("font-size", "3px"); + // // } else if (i === state.count - 2) { + // // bullet.css("font-size", "4px"); + // // } + // // } + // } + + $('').text((state.index+1)+"/"+state.count).appendTo(stepToolbar) + var nextButton; + if (fullscreen || !step.wait) { + nextButton = $('').appendTo(stepToolbar).one('click',function(evt) { + evt.preventDefault(); + stepEventListener(); + }); + if (state.index === state.count - 1) { + $('').text(RED._("common.label.close")).appendTo(nextButton); + } else if (state.index === 0) { + $('start').text(RED._("tourGuide.start")).appendTo(nextButton); + } else if (state.index < state.count-1) { + $('').text(RED._("tourGuide.next")).appendTo(nextButton); + $('').appendTo(nextButton); + } + } + + var width = step.width; + if (fullscreen) { + width = 500; + } + var maxWidth = Math.min($(window).width()-10,Math.max(width || 0, 300)); + if (!popover) { + popover = RED.popover.create({ + target: targetElement, + width: width || "auto", + maxWidth: maxWidth+"px", + direction: direction, + class: "red-ui-tourGuide-popover"+(fullscreen?" ":""), + trigger: "manual", + content: stepContent + }).open(); + } + $(document).off('keydown.red-ui-tourGuide'); + $(document).on('keydown.red-ui-tourGuide', function(evt) { + if (evt.key === "Escape" || evt.key === "Esc") { + evt.preventDefault(); + evt.stopPropagation(); + completeStep(step, state, function() { + done(false); + }); + } + }) + popover.element.toggleClass("red-ui-tourGuide-popover-full",!!fullscreen); + popover.move({ + target: targetElement, + width: width || "auto", + maxWidth: maxWidth+"px", + direction: direction, + }) + + if (nextButton) { + setTimeout(function() { + nextButton.focus(); + },50); + } + + var isSVG = targetElement[0] instanceof SVGElement; + if (step.fallback) { + focus.one("mouseenter", function(evt) { + setTimeout(function() { + focus.css({ + width: (4*dimension)+"px", + height: (4*dimension)+"px" + }) + shade.fadeOut(); + popover.move({ + target: $(".red-ui-editor"), + direction: step.fallback, + offset: 10, + transition: true + }) + // popover.element.addClass('red-ui-tourGuide-popover-bounce'); + },isSVG?0:500); + }) + } + + var stepEventListener = function() { + focus.removeClass("transition"); + targetElement.css("z-index",zIndex); + completeStep(step, state, done); + } + + if (step.wait) { + if (step.wait.type === "dom-event") { + var eventTarget = targetElement; + if (step.wait.element) { + if (typeof step.wait.element === "string") { + eventTarget = $(step.wait.element); + } else if (typeof step.wait.element === "function") { + eventTarget = step.wait.element.call(state); + } + } + var listener = { + type: step.wait.type, + target: eventTarget, + event: step.wait.event, + listener: function() { + stepEventListener(); + }, + opts: { once: true } + } + activeListeners.push(listener) + eventTarget[0].addEventListener(listener.event,listener.listener,listener.opts) + } else if (step.wait.type === "nr-event") { + var listener = { + type: step.wait.type, + event: step.wait.event, + listener: function() { + if (step.wait.filter) { + if (!step.wait.filter.apply(state,arguments)) { + return; + } + } + stepEventListener(); + } + } + activeListeners.push(listener); + RED.events.on(listener.event,listener.listener); + } + } + }) + } + + return { + load: loadTour, + run: run, + reset: function() { + RED.settings.set("editor.tours.welcome",''); + } + } + + +})(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js b/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js index 2bdae4b13..3c1e295d2 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/userSettings.js @@ -139,7 +139,8 @@ RED.userSettings = (function() { { title: "menu.label.other", options: [ - {setting:"view-show-tips",oldSettings:"menu-menu-item-show-tips",label:"menu.label.showTips",toggle:true,default:true,onchange:"core:toggle-show-tips"} + {setting:"view-show-tips",oldSettings:"menu-menu-item-show-tips",label:"menu.label.showTips",toggle:true,default:true,onchange:"core:toggle-show-tips"}, + {setting:"view-show-welcome-tours",label:"menu.label.showWelcomeTours",toggle:true,default:true} ] } ]; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss index 4b7301542..eb0582ba1 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss @@ -140,8 +140,8 @@ $workspace-button-color-focus-outline: $form-input-focus-color; $shade-color: rgba(160,160,160,0.5); - $popover-background: #333; +$popover-border: $popover-background; $popover-color: #eee; $popover-button-border-color: #bbb; $popover-button-border-color-hover: #666; @@ -295,6 +295,10 @@ $group-default-stroke: #999; $group-default-stroke-opacity: 1; $group-default-label-color: #a4a4a4; +$tourGuide-shade: $shade-color; +$tourGuide-border: #a22222; +$tourGuide-heading-color: #a22222; + // Deprecated $text-color-green: $text-color-success; $info-text-code-color: $text-color-code; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/dropdownMenu.scss b/packages/node_modules/@node-red/editor-client/src/sass/dropdownMenu.scss index e2ef1bc91..98ab3bd3b 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/dropdownMenu.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/dropdownMenu.scss @@ -132,6 +132,7 @@ .red-ui-menu-dropdown > li > a:hover, +.red-ui-menu-dropdown > li.open > a, .red-ui-menu-dropdown > li > a:focus, .red-ui-menu-dropdown-submenu:hover > a, .red-ui-menu-dropdown-submenu:focus > a { @@ -148,6 +149,7 @@ margin-top: -6px; margin-left: -1px; } + &.open > .red-ui-menu-dropdown, &:hover > .red-ui-menu-dropdown { display: block; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/header.scss b/packages/node_modules/@node-red/editor-client/src/sass/header.scss index e638506b4..bd1e78a81 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/header.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/header.scss @@ -224,6 +224,7 @@ } } > li > a:hover, + > li.open > a, > li > a:focus, > li:hover > a, > li:focus > a { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/popover.scss b/packages/node_modules/@node-red/editor-client/src/sass/popover.scss index 872f32024..e6def9efc 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/popover.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/popover.scss @@ -19,19 +19,23 @@ display: none; position: absolute; width: auto; - padding: 10px; + padding: 2px; height: auto; - background: $popover-background; - color: $popover-color; + background: var(--red-ui-popover-border); + color: var(--red-ui-popover-color); border-radius: 4px; z-index: 1000; font-family: $primary-font; font-size: 14px; line-height: 1.4em; @include component-shadow; - border-color: $popover-background; + border-color: var(--red-ui-popover-border); +} +.red-ui-popover-content { + padding: 8px; + border-radius: 2px; + background: var(--red-ui-popover-background); } - .red-ui-popover:after, .red-ui-popover:before { border: solid transparent; content: " "; @@ -61,26 +65,26 @@ .red-ui-popover.red-ui-popover-right:after { border-color: transparent; - border-right-color: $popover-background; + border-right-color: var(--red-ui-popover-border); border-width: 10px; margin-top: -10px; } .red-ui-popover.red-ui-popover-right:before { border-color: transparent; - border-right-color: $popover-background; + border-right-color: var(--red-ui-popover-border); border-width: 11px; margin-top: -11px; } .red-ui-popover.red-ui-popover-left:after { border-color: transparent; - border-left-color: $popover-background; + border-left-color: var(--red-ui-popover-border); border-width: 10px; margin-top: -10px; } .red-ui-popover.red-ui-popover-left:before { border-color: transparent; - border-left-color: $popover-background; + border-left-color: var(--red-ui-popover-border); border-width: 11px; margin-top: -11px; } @@ -88,26 +92,26 @@ .red-ui-popover.red-ui-popover-bottom:after { border-color: transparent; - border-bottom-color: $popover-background; + border-bottom-color: var(--red-ui-popover-border); border-width: 10px; margin-left: -10px; } .red-ui-popover.red-ui-popover-bottom:before { border-color: transparent; - border-bottom-color: $popover-background; + border-bottom-color: var(--red-ui-popover-border); border-width: 11px; margin-left: -11px; } .red-ui-popover.red-ui-popover-top:after { border-color: transparent; - border-top-color: $popover-background; + border-top-color: var(--red-ui-popover-border); border-width: 10px; margin-left: -10px; } .red-ui-popover.red-ui-popover-top:before { border-color: transparent; - border-top-color: $popover-background; + border-top-color: var(--red-ui-popover-border); border-width: 11px; margin-left: -11px; } @@ -116,9 +120,10 @@ .red-ui-popover-size-small { font-size: 12px; - padding: 5px 7px; line-height: 1.8em; - + .red-ui-popover-content { + padding: 1px 4px; + } &.red-ui-popover-right:after, &.red-ui-popover-left:after { border-width: 7px; margin-top: -7px; @@ -143,7 +148,7 @@ font-size: 11px; font-family: $monospace-font; margin-left: 3px; - border: 1px solid $popover-color; + border: 1px solid var(--red-ui-popover-color); border-radius:3px; padding: 1px 2px; } @@ -152,8 +157,8 @@ .red-ui-popover button.red-ui-button { &:not(.primary) { border-color: $popover-button-border-color; - background: $popover-background; - color: $popover-color !important; + background: var(--red-ui-popover-background); + color: var(--red-ui-popover-color) !important; } &:not(.primary):not(.disabled):not(.ui-button-disabled):hover { border-color: $popover-button-border-color-hover; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/style.scss b/packages/node_modules/@node-red/editor-client/src/sass/style.scss index 8901d1305..084cda954 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/style.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/style.scss @@ -69,3 +69,5 @@ @import "debug"; @import "radialMenu"; + +@import "tourGuide"; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/tourGuide.scss b/packages/node_modules/@node-red/editor-client/src/sass/tourGuide.scss new file mode 100644 index 000000000..53c920222 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/sass/tourGuide.scss @@ -0,0 +1,122 @@ +.red-ui-tourGuide-shade { + position: absolute; + top:0; + left:0; + bottom:0; + right:0; + z-index: 2000; + overflow: hidden; +} + +.red-ui-tourGuide-shade-focus { + display: block; + width: 100px; + height: 100px; + position: absolute; + z-index: 2001; + transform: translate(-50%, -50%); + border-radius: 50%; + border: 2px solid $tourGuide-border; + + &.transition { + transition: 0.4s ease; + transition-property: width,height; + } + + &.disableInteraction { + pointer-events: none; + } + + &::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + border-radius: 50%; + border: solid 6000px $tourGuide-shade; + margin-left: -6000px; + margin-top: -6000px; + pointer-events: none; + } +} +.red-ui-popover.red-ui-tourGuide-popover { + z-index: 2003; + --red-ui-popover-background: #{$secondary-background}; + --red-ui-popover-border: #{$tourGuide-border}; + --red-ui-popover-color: #{$primary-text-color}; + + .red-ui-popover-content { + h2 { + text-align: center; + margin-top: 0px; + color: #a22222; + i.fa { + font-size: 1.5em + } + } + } + +} + +.red-ui-tourGuide-toolbar { + min-height: 36px; + position: relative; + display: flex; + align-items: flex-end; +} +.red-ui-tourGuide-breadcrumbs { + flex-grow: 1; + + display: flex; + align-items: center; + justify-content: center; + font-size: 6px; + & > div { + display: inline-block; + } + i { + line-height: 16px; + margin: 0 3px; + } +} +.red-ui-tourGuide-popover-description { + padding: 10px 20px 5px; +} +.red-ui-tourGuide-popover-full { + .red-ui-tourGuide-popover-description { + padding: 20px 40px 10px; + text-align: center; + } +} +.red-ui-popover.red-ui-tourGuide-popover button.red-ui-button { + &:not(.primary) { + border-color: transparent; + background: $secondary-background; + color: $primary-text-color !important; + } + &:not(.primary):not(.disabled):not(.ui-button-disabled):hover { + border-color: $popover-button-border-color-hover; + } +} + + +// .red-ui-tourGuide-popover-bounce { +// animation: 10s ease-in 5s infinite both red-ui-tourGuide-popover-bounce; +// } +// // @keyframes *must* be on multiple lines so build-custom-theme can filter them out +// @keyframes red-ui-tourGuide-popover-bounce { +// 0%, +// 10%, +// 100% { +// -webkit-transform: translateY(0); +// transform: translateY(0); +// } +// 2%,8% { +// -webkit-transform: translateY(-5px); +// transform: translateY(-5px); +// } +// 5% { +// -webkit-transform: translateY(5px); +// transform: translateY(5px); +// } +// } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/variables.scss b/packages/node_modules/@node-red/editor-client/src/sass/variables.scss index 72b573a2e..1ddf08411 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/variables.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/variables.scss @@ -81,12 +81,15 @@ --red-ui-node-status-changed-border: #{$node-status-changed-border}; --red-ui-node-status-changed-background: #{$node-status-changed-background}; - - --red-ui-node-border: #{$node-border}; --red-ui-node-port-background:#{$node-port-background}; --red-ui-node-label-color: #{$node-label-color}; --red-ui-node-selected-color: #{$node-selected-color}; --red-ui-port-selected-color: #{$port-selected-color}; + + --red-ui-popover-background: #{$popover-background}; + --red-ui-popover-border: #{$popover-border}; + --red-ui-popover-color: #{$popover-color}; + } diff --git a/packages/node_modules/@node-red/editor-client/src/tours/first-flow.js b/packages/node_modules/@node-red/editor-client/src/tours/first-flow.js new file mode 100644 index 000000000..e7eed1d0f --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/tours/first-flow.js @@ -0,0 +1,80 @@ +export default { + steps: [ + { + title: "Create your first flow", + width: 400, + description: 'This tutorial will guide you through creating your first flow', + nextButton: 'start' + }, + { + element: "#red-ui-workspace .red-ui-tab-button.red-ui-tabs-add", + description: 'To add a new tab, click the button', + wait: { + type: "dom-event", + event: "click", + element: "#red-ui-workspace .red-ui-tab-button.red-ui-tabs-add a" + }, + }, + { + element: '.red-ui-palette-node[data-palette-type="inject"]', + direction: 'right', + description: 'The palette lists all of the nodes available to use. Drag a new Inject node into the workspace.', + fallback: 'inset-bottom-right', + wait: { + type: "nr-event", + event: "nodes:add", + filter: function(event) { + if (event.type === "inject") { + this.injectNode = event; + return true; + } + return false + } + }, + complete: function() { + $('.red-ui-palette-node[data-palette-type="inject"]').css("z-index","auto"); + } + }, + { + element: '.red-ui-palette-node[data-palette-type="debug"]', + direction: 'right', + description: 'Next, drag a new Debug node into the workspace.', + fallback: 'inset-bottom-right', + wait: { + type: "nr-event", + event: "nodes:add", + filter: function(event) { + if (event.type === "debug") { + this.debugNode = event; + return true; + } + return false + } + }, + complete: function() { + $('.red-ui-palette-node[data-palette-type="debug"]').css("z-index","auto"); + }, + }, + { + element: function() { return $("#"+this.injectNode.id+" .red-ui-flow-port") }, + description: 'Add a wire from the output of the Inject node to the input of the Debug node', + fallback: 'inset-bottom-right', + wait: { + type: "nr-event", + event: "links:add", + filter: function(event) { + return event.source.id === this.injectNode.id && event.target.id === this.debugNode.id; + } + }, + }, + { + element: "#red-ui-header-button-deploy", + description: 'Deploy your changes so the flow is active in the runtime', + width: 200, + wait: { + type: "dom-event", + event: "click" + }, + } + ] +} diff --git a/packages/node_modules/@node-red/editor-client/src/tours/welcome.js b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js new file mode 100644 index 000000000..f425a3f1e --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/tours/welcome.js @@ -0,0 +1,70 @@ +export default { + version: "2.1.0", + steps: [ + { + titleIcon: "fa fa-map-o", + title: { "en-US": "Welcome to Node-RED 2.1!" }, + description: { "en-US": "Let's take a moment to discover the new features in this release." } + }, + { + title: { "en-US": "A new Tour Guide" }, + description: { "en-US": "

First, as you've already found, we now have this tour of new features. We'll only show the tour the first time you open the editor for each new version of Node-RED.

"+ + "

You can choose not to see this tour in the future by disabling it under the View tab of User Settings.

" } + }, + { + prepare() { + $("#red-ui-header-button-sidemenu").trigger("click"); + $("#menu-item-edit-menu").parent().addClass("open") + }, + complete() { + $("#menu-item-edit-menu").parent().removeClass("open") + }, + element: "#menu-item-edit-menu-submenu", + interactive: false, + direction: "left", + title: { "en-US": "New Edit menu" }, + description: { "en-US": "

The main menu has been updated with a new 'Edit' section. This includes all of the familar options, like cut/paste and undo/redo.

"+ + "

The menu now displays keyboard shortcuts for the options.

" } + + }, + { + prepare() { + $("#red-ui-header-button-sidemenu").trigger("click"); + $("#menu-item-arrange-menu").parent().addClass("open") + }, + complete() { + $("#menu-item-arrange-menu").parent().removeClass("open") + }, + element: "#menu-item-arrange-menu-submenu", + interactive: false, + direction: "left", + title: { "en-US": "Arranging nodes" }, + description: { "en-US": "

The new 'Arrange' section of the menu provides new options to help arrange your nodes. You can align them to a common edge, spread them out evenly or change their order.

" }, + }, + { + element: "#red-ui-workspace-tabs > li:first-child", + title: { "en-US": "Flow and Group level environment variables" }, + description: { "en-US": "

Flows and Groups can now have their own environment variables that can be referenced by nodes inside them.

" }, + }, + { + prepare(done) { + RED.editor.editFlow(RED.nodes.workspace(RED.workspaces.active()),"editor-tab-envProperties"); + setTimeout(done,800); + }, + element: "#red-ui-tab-editor-tab-envProperties-link-button", + description: { "en-US": "

Flows and Groups now have an Environment Variables section in their edit dialog.

" }, + }, + { + element: ".node-input-env-container-row .red-ui-editableList-addButton", + direction: "top", + description: { "en-US": '

The environment variables are listed in this table and new ones can be added by clicking the button.

' }, + complete() { + $("#node-dialog-cancel").trigger("click"); + } + }, + { + title: { "en-US": "And that's not all..." }, + description: { "en-US": "

There's more still to come before 2.1.0 is released. Watch this space!

" } + }, + ] +}