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/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..772c59b3c --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tour/tourGuide.js @@ -0,0 +1,360 @@ +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); + } + }; + if (tourCache[tourPath]) { + runTour(tourCache[tourPath],done); + } else { + import(tourPath).then(function(module) { + tourCache[tourPath] = module.default; + runTour(tourCache[tourPath],done); + }).catch(function(err) { + console.warn("Error loading tour:",err); + done(err); + }) + } + } + + 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" + }) + 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"); + 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 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") + } + + 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(step.title).appendTo(stepDescription); + } + $('
').css("text-align","left").html(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) + if (fullscreen || !step.wait) { + var nextButton = $('').appendTo(stepToolbar).click(function(evt) { + evt.preventDefault(); + stepEventListener(); + }); + if (state.index === state.count - 1) { + $('close').appendTo(nextButton); + } else if (state.index === 0) { + $('start').appendTo(nextButton); + } else if (state.index < state.count-1) { + $('next ').appendTo(nextButton); + } + } + + var width = step.width; + if (fullscreen) { + width = 500; + } + var maxWidth = 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(); + } + popover.element.toggleClass("red-ui-tourGuide-popover-full",!!fullscreen); + popover.move({ + target: targetElement, + width: width || "auto", + maxWidth: maxWidth+"px", + direction: direction, + }) + + + 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 { + run: run + } + + +})(); 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 28aab75ef..b397e05b7 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 @@ -295,6 +295,10 @@ $group-default-stroke: #999; $group-default-stroke-opacity: 1; $group-default-label-color: #a4a4a4; +$tourGuide-shade: rgba(100, 70, 70, 0.6); +$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/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/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-2-1.js b/packages/node_modules/@node-red/editor-client/src/tours/welcome-2-1.js new file mode 100644 index 000000000..33f6a1a88 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/tours/welcome-2-1.js @@ -0,0 +1,69 @@ +export default { + steps: [ + { + titleIcon: "fa fa-map-o", + title: "Welcome to Node-RED 2.1!", + description: "Let's take a moment to discover the new features in this release." + }, + { + title: "A new Tour Guide", + description: "

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:function() { + $("#red-ui-header-button-sidemenu").trigger("click"); + $("#menu-item-edit-menu").parent().addClass("open") + }, + complete: function() { + $("#menu-item-edit-menu").parent().removeClass("open") + }, + element: "#menu-item-edit-menu-submenu", + interactive: false, + direction: "left", + title: "New Edit menu", + description: "

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: function() { + $("#red-ui-header-button-sidemenu").trigger("click"); + $("#menu-item-arrange-menu").parent().addClass("open") + }, + complete: function() { + $("#menu-item-arrange-menu").parent().removeClass("open") + }, + element: "#menu-item-arrange-menu-submenu", + interactive: false, + direction: "left", + title: "Arranging nodes", + description: "

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: "Flow and Group level environment variables", + description: "

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

", + }, + { + prepare: function(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: "

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: '

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

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

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

" + }, + ] +}