diff --git a/package.json b/package.json index a0c7395..721cd4b 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "fast.js": "^0.1.1", "gl": "^2.1.5", "glView-helpers": "usco/glView-helpers", + "hopscotch": "^0.2.6", "jsondiffpatch": "^0.1.41", "log-minim": "usco/log-minim", "loglevel": "^1.2.0", diff --git a/src/components/main/app.css b/src/components/main/app.css index 29ff757..0920695 100644 --- a/src/components/main/app.css +++ b/src/components/main/app.css @@ -257,6 +257,12 @@ button[disabled=disabled]:hover, button:disabled:hover{ opacity: 1!important; } +#overlayContainer{ + position: absolute; + top: 20%; + left: 50%; +} + .innerWrapper{ opacity: 0; transition: opacity 1.0s ease-in-out; diff --git a/src/components/main/index.js b/src/components/main/index.js index 69a39ec..6b64e58 100644 --- a/src/components/main/index.js +++ b/src/components/main/index.js @@ -5,6 +5,7 @@ const {just} = Rx.Observable import Settings from '../../components/Settings' import FullScreenToggler from '../../components/widgets/FullScreenToggler/index' import Help from '../../components/widgets/Help' +import FeatureTour from '../../components/widgets/FeatureTour/index' import { EntityInfosWrapper, BOMWrapper, GLWrapper, CommentsWrapper, progressBarWrapper } from '../../components/main/wrappers' @@ -31,10 +32,11 @@ export default function main (sources) { const settingsC = Settings({DOM, props$: state$}) const fsToggler = FullScreenToggler({DOM}) const help = Help({DOM, props$: state$}) + const featureTour = FeatureTour({DOM, props$: state$}) // outputs const vtree$ = view(state$, settingsC.DOM, fsToggler.DOM, bom.DOM, gl.DOM - , entityInfos.DOM, comments.DOM, help.DOM) + , entityInfos.DOM, comments.DOM, help.DOM, featureTour.DOM) const events$ = just({ gl: gl.events, entityInfos: entityInfos.events, @@ -50,7 +52,6 @@ export default function main (sources) { // save file to user hdd const fileStorage$ = formatDataForFileStorage({sources, state$}, bom) - // return anything you want to output to sources return { DOM: vtree$, diff --git a/src/components/main/view.js b/src/components/main/view.js index e1b9c74..9083393 100644 --- a/src/components/main/view.js +++ b/src/components/main/view.js @@ -131,11 +131,11 @@ function renderRightToolbar (state, {bom}) { function renderTopToolbar (state) { const progressBar = renderProgressBar({progress: state.operationsInProgress * 100}) - return h('section#topToolBar', [ h('section.notifications', [state.notifications]), progressBar ]) + return h('section#topToolBar', [h('section.notifications', [state.notifications]), progressBar ]) } function renderUiElements (uiElements) { - const {state, settings, fsToggler, bom, gl, entityInfos, help} = uiElements + const {state, settings, fsToggler, bom, gl, entityInfos, help, featureTour} = uiElements const widgetsMapping = { //'comments': comments, @@ -152,6 +152,7 @@ function renderUiElements (uiElements) { const rightToolbar = renderRightToolbar(state, uiElements) const bottomToolBar = h('section#bottomToolBar', [settings, help, fsToggler]) const topToolbar = renderTopToolbar(state) + const overlayContainer = h('div#overlayContainer') return h('div.jam', flatten([ gl, @@ -159,7 +160,8 @@ function renderUiElements (uiElements) { leftToolbar, rightToolbar, topToolbar, - bottomToolBar + bottomToolBar, + overlayContainer ])) } @@ -174,8 +176,9 @@ function widgetNamesByToolSet (toolset) { } export default function view(state$, settings$, fsToggler$, bom$ - , gl$, entityInfos$, comment$, help$) { + , gl$, entityInfos$, comment$, help$, featureTour$) { - return combineLatestObj({state$, settings$, fsToggler$, bom$, gl$, entityInfos$, comment$, help$}) + return combineLatestObj({state$, settings$, fsToggler$, bom$ + , gl$, entityInfos$, comment$, help$, featureTour$}) .map(renderUiElements) } diff --git a/src/components/widgets/FeatureTour/featureTour.css b/src/components/widgets/FeatureTour/featureTour.css new file mode 100644 index 0000000..b382e82 --- /dev/null +++ b/src/components/widgets/FeatureTour/featureTour.css @@ -0,0 +1,136 @@ +.hopscotch-bubble-container{ + display: inline-block; + font-family: 'Open Sans', sans-serif; + z-index: 1000; + background-color: white; + /*width: 700px !important;*/ + border: 1px solid #dddddd; + font-size: 0.7em; +} + +.hopscotch-bubble-number{ + display: none; +} + +.hopscotch-title{ + margin-top: 5px; +} + +.hopscotch-content{ + display: inline-block; + width: 100% +} + +.featureTourImage{ + display: block; +} +.imageLeft{ + float: left; + margin-right: 15px; +} +.imageRight{ + float: right; + margin-left: 15px; +} + +.hopscotch-actions{ + display: block; + margin-top: 10px; + float: right; +} + +.hopscotch-nav-button{ + display: inline-block; + background: #5fc5fb; + color: #ffffff; +} + +.hopscotch-bubble-close, .hopscotch-bubble-close:hover{ + position: absolute; + top: 15; + right: 15; + width: 20px; height: 20px; + background: #F5F5F5 url("data:image/svg+xml;utf8,") !important; + text-indent: -9999px; +} + +.hidden{ + display: none; +} + +.hopscotch-bubble-arrow-container { + position: absolute; + width: 15px; + height: 15px; +} +.hopscotch-bubble-arrow-container .hopscotch-bubble-arrow, +.hopscotch-bubble-arrow-container .hopscotch-bubble-arrow-border { + width: 0; + height: 0; +} +.hopscotch-bubble-arrow-container.up { + top: -15px; + left: 7px; +} +.hopscotch-bubble-arrow-container.up .hopscotch-bubble-arrow { + border-bottom: 15px solid #fff; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + position: relative; + top: -14px; +} +.hopscotch-bubble-arrow-container.up .hopscotch-bubble-arrow-border { + border-bottom: 15px solid #dddddd; + border-left: 15px solid transparent; + border-right: 15px solid transparent; +} +.hopscotch-bubble-arrow-container.down { + bottom: -15px; + left: 7px; +} +.hopscotch-bubble-arrow-container.down .hopscotch-bubble-arrow { + border-top: 15px solid #fff; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + position: relative; + bottom: 16px; +} +.hopscotch-bubble-arrow-container.down .hopscotch-bubble-arrow-border { + border-top: 15px solid #dddddd; + border-left: 15px solid transparent; + border-right: 15px solid transparent; +} +.hopscotch-bubble-arrow-container.left { + top: 10px; + left: -15px; +} +.hopscotch-bubble-arrow-container.left .hopscotch-bubble-arrow { + border-bottom: 15px solid transparent; + border-right: 15px solid #fff; + border-top: 15px solid transparent; + position: relative; + left: 1px; + top: -30px; +} +.hopscotch-bubble-arrow-container.left .hopscotch-bubble-arrow-border { + border-right: 15px solid #dddddd; + border-bottom: 15px solid transparent; + border-top: 15px solid transparent; +} +.hopscotch-bubble-arrow-container.right { + top: 10px; + right: -15px; +} +.hopscotch-bubble-arrow-container.right .hopscotch-bubble-arrow { + border-bottom: 15px solid transparent; + border-left: 15px solid #fff; + border-top: 15px solid transparent; + position: relative; + right: 1px; + top: -30px; +} +.hopscotch-bubble-arrow-container.right .hopscotch-bubble-arrow-border { + border-left: 15px solid #dddddd; + border-bottom: 15px solid transparent; + border-top: 15px solid transparent; +} diff --git a/src/components/widgets/FeatureTour/images/bunny.jpg b/src/components/widgets/FeatureTour/images/bunny.jpg new file mode 100644 index 0000000..8e01d75 Binary files /dev/null and b/src/components/widgets/FeatureTour/images/bunny.jpg differ diff --git a/src/components/widgets/FeatureTour/images/solar_system.jpg b/src/components/widgets/FeatureTour/images/solar_system.jpg new file mode 100644 index 0000000..1c67692 Binary files /dev/null and b/src/components/widgets/FeatureTour/images/solar_system.jpg differ diff --git a/src/components/widgets/FeatureTour/index.js b/src/components/widgets/FeatureTour/index.js new file mode 100644 index 0000000..3982d49 --- /dev/null +++ b/src/components/widgets/FeatureTour/index.js @@ -0,0 +1,54 @@ +import { combineLatestObj } from '../../../utils/obsUtils' +import { h } from '@cycle/dom' +import startUpTour from './startUpTour.js' +import hopscotch from 'hopscotch' +require('./featureTour.css') +import rx from 'Rx' + +function intent (DOM) { + const closeSelection = new rx.ReplaySubject() + hopscotch.registerHelper('clearState', function () { + closeSelection.onNext(false) + }) + + let selected$ = DOM.select('.startUpTourLink').events('click') + .map(true) + .startWith(false) + .merge(closeSelection) + return {selected$} +} + +function model (props$, actions) { + const selected$ = actions.selected$.startWith(false) + const firstRun$ = props$.map(e => false) // needs to change later + return combineLatestObj({selected$, firstRun$}) +} + +function view (state$) { + return state$.map(function (state) { + let active = state.selected || state.firstRun + // you can also use this to load other feature tours then just the startup tour + let tour = startUpTour() + + hopscotch.registerHelper('hideArrow', function () { + document.querySelector('.hopscotch-bubble-arrow-container').className += ' hidden' + }) + if (active) { + hopscotch.startTour(tour) + + // this is nescessary to fix an initial calculation bug in hopscotch + if (parseInt(document.querySelector('.hopscotch-bubble').style.left) < 0) { + document.querySelector('.hopscotch-bubble').style.left = '25%' + } + } + }) +} + +export default function FeatureTour ({DOM, props$}) { + let actions = intent(DOM) + let state$ = model(props$, actions) + let vtree$ = view(state$) + return { + DOM: vtree$ + } +} diff --git a/src/components/widgets/FeatureTour/startUpTour.js b/src/components/widgets/FeatureTour/startUpTour.js new file mode 100644 index 0000000..9738a47 --- /dev/null +++ b/src/components/widgets/FeatureTour/startUpTour.js @@ -0,0 +1,145 @@ +export default function startUpTour () { + const imagePath = './src/components/widgets/FeatureTour/images/' + // look here on how to position the bubbles: http://linkedin.github.io/hopscotch/ + // if the bubble goes off the screen by how it is positoned relative to the target, + // use xOffset, yOffseten and arrowOffset to position it within the screen again. + const content = { + id: 'startupTour', + showPrevButton: true, + bubbleWidth: 450, + // it's very important to clear the state on close and on end. Don't remove this. + onClose: ['clearState'], + onEnd: ['clearState'], + steps: [ + { + title: 'Welcome at the first-run tour of JAM.', + content: `
The purpose of this tour is to show you how to make the best use of JAM. This is a callout
`, + target: document.querySelector('#overlayContainer'), + placement: 'bottom', + xOffset: 'center', + arrowOffset: 'center', + // use the hideArrow helper to hide the arrow and create a call out. + onShow: ['hideArrow'] + }, + { + title: 'Assemblies', + content: `You can use JAM to make assemblies. In an assembly you put your parts together as they would be in real life. This way other people can see what the end result looks like and as a bonus other people can see how to put it together themselves. placement bottom, xoffset center `, + target: document.querySelector('#overlayContainer'), + placement: 'bottom', + xOffset: 'center', + arrowOffset: 'center' + }, + { + title: 'My content', + content: 'placement right (this step only shows when no model is loaded) First let’s load a model by dragging it into JAM or click here to load an example model.', + target: document.querySelector('.toMirrorMode'), + placement: 'right' + }, + { + title: 'My content', + content: 'placement top, xoffset right You select a part by clicking it. When a part is selected, it has a thick black outline. Unselect the part by clicking it again.', + target: document.querySelector('.settingsToggler'), + placement: 'top', + xOffset: -425, + arrowOffset: 425 + }, + { + title: 'My content', + content: 'Click here to see a list of all of your parts.', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'You can also add parts which do not exist as a 3D file, for example tape, bolts or glue. These parts are usually not-printable.', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'When you click one of these you can copy the bill of materials as a text- or json file. You can look at this file as a to-print-and-to-get-list.', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'Here on the left, you find all the tools which you use to edit or manipulate your model. Pro tip: You can manipulate multiple parts at the same time by simply selecting multiple parts. ', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'With the translate tool, you move parts around on the build area. When snap translation is selected, the position snaps to rounded mm’s.', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'You use the rotate tool to make sure that your parts are in the right orientation. When snap snap rotation is selected, it snaps to rounded degrees.', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'You can probably already guess what the scaling and the snap-scaling functionalities do. Uniform scaling makes sure that you can scale the model, but it stays in its original dimensions. So all sides are scaled equally. ', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'With the mirror tool, you can mirror you part along the x, y and z axis.', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'These two tools are used to duplicate or delete a part.', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'Give your parts different colours to make your assembly even more life-like. ', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'Perform measureents', + content: 'The last of the tools is to perform measurements on your model. You could for example measure how wide a part is and compare that to other parts.', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'In the bottom right, you find everything regarding help and settings. ', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'With this button you can toggle between the regular screen and a full screen view. ', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'You can access various settings here. ', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'Click here for extra information on JAM or to start this startup tour again. ', + target: document.querySelector('.bomtoggler'), + placement: 'left' + }, + { + title: 'My content', + content: 'This is the end of the start-up tour. Thank you for joining.', + target: document.querySelector('.bomtoggler'), + placement: 'left' + } + ] + } + return content +} diff --git a/src/components/widgets/Help/help.css b/src/components/widgets/Help/help.css index 2f1d4e1..7363568 100644 --- a/src/components/widgets/Help/help.css +++ b/src/components/widgets/Help/help.css @@ -34,3 +34,7 @@ display: block; line-height: 1; } + +.startUpTourLink .textLink{ + font-size: 1em; +} diff --git a/src/components/widgets/Help/index.js b/src/components/widgets/Help/index.js index 2d04c5a..b814e21 100644 --- a/src/components/widgets/Help/index.js +++ b/src/components/widgets/Help/index.js @@ -68,6 +68,11 @@ function view (state$) {