Skip to content

Commit 59ce036

Browse files
adamcarhedenMarkusBordihn
authored andcommitted
Added tour functionality to tutorial (#204)
1 parent ad28308 commit 59ce036

File tree

32 files changed

+516
-27
lines changed

32 files changed

+516
-27
lines changed

build/cwc/tutorials.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,26 @@ let replacePlaceholders = function(obj, pwd) {
6363
if (obj === null || typeof obj !== 'object') {
6464
return;
6565
}
66-
let templateRE = /^___TEMPLATE___:(binary:)?/;
66+
let templateRE = /^___TEMPLATE___:((json|binary):)?/;
6767
for (let k in obj) {
6868
if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
6969
const matches = k.match(templateRE);
7070
if (matches) {
71-
const data = matches[1] ?
72-
fs.readFileSync(pwd+'/'+obj[k]).toString('base64') :
73-
fs.readFileSync(pwd+'/'+obj[k], 'utf8');
71+
let data = '';
72+
switch (matches[2]) {
73+
case 'binary':
74+
data = fs.readFileSync(pwd+'/'+obj[k]).toString('base64');
75+
break;
76+
case 'json':
77+
data = JSON.parse(fs.readFileSync(pwd+'/'+obj[k], 'utf8'));
78+
break;
79+
case undefined:
80+
data = fs.readFileSync(pwd+'/'+obj[k], 'utf8');
81+
break;
82+
default:
83+
throw new Error('Unknown file type "' + matches[2] +
84+
'" for template "'+k+'"');
85+
}
7486
obj[k.replace(matches[0], '')] = data;
7587
delete obj[k];
7688
} else {
@@ -95,7 +107,8 @@ procTemplates(inDir, function(template) {
95107
if (err) {
96108
throw err;
97109
}
98-
console.log(template.replace(base, ''), '->', target.replace(base, ''));
110+
console.log('\t'+template.replace(base, ''), '->',
111+
target.replace(base, ''));
99112
});
100113
});
101114
});

src/ui/select_screen/normal/basic/basic.soy

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@
118118
{/call}
119119

120120
<div class="mdl-grid cwc-tutorial-list">
121+
{call cwc.soy.SelectScreenTemplate.fileCard data="all"}
122+
{param title: 'Introduction to Coding with Chrome' /}
123+
{param text: 'Everything you need to know to get started.' /}
124+
{param opt_link_text: 'Learn CwC' /}
125+
{param tutorial: 'cwc.cwct' /}
126+
{param opt_color_class: 'bg-light-blue' /}
127+
{param opt_icon: 'school' /}
128+
{/call}
129+
121130
{call cwc.soy.SelectScreenTemplate.fileCard data="all"}
122131
{param title: 'Blockly Basics' /}
123132
{param text: 'Learn drag-and-drop programming' /}

src/ui/tour/tour.js

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,6 @@ cwc.ui.Tour.prototype.setTour = function(tour) {
7575
'showCancelLink': true,
7676
},
7777
});
78-
this.tour_['once']('cancel', () => {
79-
let sidebarInstance = this.helper.getInstance('sidebar');
80-
if (sidebarInstance) {
81-
sidebarInstance.setActive('tour', false);
82-
}
83-
});
8478
this.tourLength_ = tour['data'].length;
8579
for (let i in tour['data']) {
8680
if (!Object.prototype.hasOwnProperty.call(tour['data'], i)) continue;
@@ -128,13 +122,25 @@ cwc.ui.Tour.prototype.getTourDescription = function() {
128122
return this.tourDescription_ || '';
129123
};
130124

125+
/**
126+
* @return {Shepherd.Tour|boolean}
127+
*/
128+
cwc.ui.Tour.prototype.getTour = function() {
129+
return this.tour_ || false;
130+
};
131131

132132
cwc.ui.Tour.prototype.startTour = function() {
133133
if (!this.tour_) {
134134
return;
135135
}
136136
this.tour_.cancel();
137137
this.log_.info('Starting tour with', this.tourLength_, 'steps...');
138+
this.tour_['once']('cancel', () => {
139+
let sidebarInstance = this.helper.getInstance('sidebar');
140+
if (sidebarInstance) {
141+
sidebarInstance.setActive('tour', false);
142+
}
143+
});
138144
let sidebarInstance = this.helper.getInstance('sidebar');
139145
if (sidebarInstance) {
140146
sidebarInstance.showContent('tour', 'Tour', this.tourDescription_);
@@ -160,47 +166,65 @@ cwc.ui.Tour.prototype.clear = function() {
160166
this.tourLength_ = 0;
161167
};
162168

163-
164169
/**
165-
* @param {number} step
170+
* @param {number} stepNumber
171+
* @param {number} tourLength
172+
* @param {function} cancel
166173
* @return {!array}
167174
* @private
168175
*/
169-
cwc.ui.Tour.prototype.processButtons_ = function(step) {
176+
cwc.ui.Tour.prototype.getStepButtons = function(stepNumber, tourLength,
177+
cancel) {
170178
let tourButtons = [];
171179
// Back button
172-
if (step > 0) {
180+
if (stepNumber > 0) {
173181
tourButtons.push({
174182
'text': i18t('@@GENERAL__BACK'),
175-
'action': this.tour_['back'],
183+
// The inline func lets us call getStepButtons before this.tour_ is set
184+
'action': () => {
185+
this.tour_['back']();
186+
},
176187
'classes': 'shepherd-button-secondary',
177188
});
178189
}
179190

180191
// Exit
181-
if (step == 0) {
192+
if (stepNumber == 0) {
182193
tourButtons.push({
183194
'text': i18t('@@GENERAL__EXIT'),
184-
'action': this.cancelTour.bind(this),
195+
'action': cancel,
185196
'classes': 'shepherd-button-secondary',
186197
});
187198
}
188199

189200
// Done
190-
if (step == this.tourLength_ - 1) {
201+
if (stepNumber == tourLength - 1) {
191202
tourButtons.push({
192203
'text': i18t('@@GENERAL__DONE'),
193-
'action': this.cancelTour.bind(this),
204+
'action': cancel,
194205
'classes': 'shepherd-button-example-primary',
195206
});
196207
}
197208
// Next button
198-
if (step < this.tourLength_ - 1) {
209+
if (stepNumber < tourLength - 1) {
199210
tourButtons.push({
200211
'text': i18t('@@GENERAL__NEXT'),
201-
'action': this.tour_['next'],
212+
// The inline func lets us call getStepButtons before this.tour_ is set
213+
'action': () => {
214+
this.tour_['next']();
215+
},
202216
'classes': 'shepherd-button-example-primary',
203217
});
204218
}
205219
return tourButtons;
206220
};
221+
222+
/**
223+
* @param {number} stepNumber
224+
* @return {!array}
225+
* @private
226+
*/
227+
cwc.ui.Tour.prototype.processButtons_ = function(stepNumber) {
228+
return this.getStepButtons(stepNumber, this.tourLength_,
229+
this.cancelTour.bind(this));
230+
};

src/ui/tutorial/tutorial.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ cwc.ui.Tutorial = function(helper) {
116116

117117
/** @private {Element} */
118118
this.rootNode_ = null;
119+
120+
/** @private {cwc.ui.Tour} */
121+
this.tour_ = this.helper.getInstance('tour');
119122
};
120123

121124

@@ -229,6 +232,24 @@ cwc.ui.Tutorial.prototype.addStep_ = async function(stepTemplate, id) {
229232
await this.appendBinaries_(stepTemplate['images'], images, 'image');
230233
}
231234

235+
let tour = false;
236+
if (stepTemplate['tour']) {
237+
if (Array.isArray(stepTemplate['tour'])) {
238+
tour = stepTemplate['tour'];
239+
tour.forEach((step, stepNumber) => {
240+
if (!('buttons' in step)) {
241+
step['buttons'] = this.tour_.getStepButtons(stepNumber, tour.length,
242+
() => {
243+
this.cancelTour_();
244+
});
245+
}
246+
});
247+
} else {
248+
this.log_.warn('Skipping tour for step', id, 'because it is not an array',
249+
stepTemplate['tour']);
250+
}
251+
}
252+
232253
this.steps_[id] = {
233254
code: code,
234255
description: description,
@@ -237,6 +258,7 @@ cwc.ui.Tutorial.prototype.addStep_ = async function(stepTemplate, id) {
237258
title: stepTemplate['title'] || '',
238259
validate: stepTemplate['validate'] || false,
239260
videos: stepTemplate['videos'] || [],
261+
tour: tour,
240262
};
241263
};
242264

@@ -444,6 +466,7 @@ cwc.ui.Tutorial.prototype.startTutorial = function() {
444466
video['youtube_id']
445467
),
446468
hasCode: step.code && step.code.trim().length > 0,
469+
hasTour: !!step.tour,
447470
})),
448471
}
449472
);
@@ -533,6 +556,7 @@ cwc.ui.Tutorial.prototype.initSteps_ = function() {
533556
step.node = stepNode;
534557
step.nodeContinue = stepNode.querySelector(classPrefix + 'continue');
535558
step.nodeLoadCode = stepNode.querySelector(classPrefix + 'load-code');
559+
step.nodeLoadTour = stepNode.querySelector(classPrefix + 'load-tour');
536560
step.nodeHeader = stepNode.querySelector(classPrefix + 'header');
537561
step.nodeListMediaExpand = stepNode.querySelectorAll(classPrefix +
538562
'media-expand');
@@ -557,6 +581,10 @@ cwc.ui.Tutorial.prototype.initStepButtons_ = function() {
557581
goog.events.listen(step.nodeLoadCode, goog.events.EventType.CLICK,
558582
this.loadCodeWithPrompt_.bind(this));
559583
}
584+
if (step.nodeLoadTour) {
585+
goog.events.listen(step.nodeLoadTour, goog.events.EventType.CLICK,
586+
this.loadTour.bind(this));
587+
}
560588
goog.events.listen(step.nodeHeader, goog.events.EventType.CLICK,
561589
this.jumpToStep_.bind(this, step.id));
562590

@@ -602,8 +630,18 @@ cwc.ui.Tutorial.prototype.jumpToStep_ = function(stepID) {
602630
activeStepID: stepID,
603631
});
604632
}
633+
this.cancelTour_();
605634
};
606635

636+
/**
637+
* @private
638+
*/
639+
cwc.ui.Tutorial.prototype.cancelTour_ = function() {
640+
let tour = this.tour_.getTour();
641+
if (tour) {
642+
tour.cancel();
643+
}
644+
};
607645

608646
/**
609647
* Scrolls the tutorial to the top of the given step.
@@ -747,10 +785,14 @@ cwc.ui.Tutorial.prototype.showMedia_ = function(media) {
747785
* @private
748786
*/
749787
cwc.ui.Tutorial.prototype.setState_ = function(change) {
788+
let previousActiveStepID = this.state_.activeStepID;
750789
let isEditorDirty = this.isEditorDirty_();
751790
Object.keys(change).forEach((key) => {
752791
this.state_[key] = change[key];
753792
});
793+
if (previousActiveStepID != this.state_.activeStepID) {
794+
this.cancelTour_();
795+
}
754796
this.updateView_();
755797
if (!isEditorDirty) {
756798
this.loadCode_();
@@ -807,6 +849,7 @@ cwc.ui.Tutorial.prototype.loadCodeWithPrompt_ = function() {
807849
});
808850
};
809851

852+
810853
/**
811854
* Loads example code into editor
812855
* @private
@@ -825,6 +868,28 @@ cwc.ui.Tutorial.prototype.loadCode_ = function() {
825868
this.restartValidate_();
826869
};
827870

871+
/**
872+
* Starts a per-step tour
873+
* @private
874+
*/
875+
cwc.ui.Tutorial.prototype.loadTour = function() {
876+
let step = this.getActiveStep_();
877+
if (!step.tour) {
878+
this.log_.warn('loadTour called for step with no tour');
879+
return;
880+
}
881+
if (!this.tour_) {
882+
this.log_.error('No tour instnace, can\'t load tour');
883+
return;
884+
}
885+
this.tour_.setTour({
886+
'description': 'Tutorial Tour', // TODO(carheden): Merge tutorial/tour
887+
'data': step.tour,
888+
});
889+
this.cancelTour_();
890+
this.tour_.getTour().start();
891+
};
892+
828893
/**
829894
* Updates the view to reflect the current state
830895
* @private

src/ui/tutorial/tutorial.soy

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
{@param images: list<string>}
7878
{@param isLastStep: bool}
7979
{@param hasCode: bool}
80+
{@param hasTour: bool}
8081
{@param number: number}
8182
{@param online: bool}
8283
{@param prefix: string}
@@ -124,11 +125,16 @@
124125
<div class="{$prefix}actions">
125126
{if $hasCode}
126127
<button type="button" class="mdl-button mdl-js-button mdl-button--colored mdl-button--raised {$prefix}load-code">
127-
<i class='material-icons'>chevron_left</i><label>Load Example Code</label>
128+
<i class='material-icons'>chevron_left</i><label>{msg desc="Overwrite the content of the editor with the example code for the current step"}Load Example Code{/msg}</label>
129+
</button>
130+
{/if}
131+
{if $hasTour}
132+
<button type="button" class="mdl-button mdl-js-button mdl-button--colored mdl-button--raised {$prefix}load-tour">
133+
<label>{msg desc="Shows the user what to do by opening a set of modal dialogs that point to areas of the scren"}Show Me{/msg}</label>
128134
</button>
129135
{/if}
130136
{if not $isLastStep}
131-
<button type="button" class="mdl-button mdl-js-button mdl-button--colored mdl-button--raised {$prefix}continue">Continue</button>
137+
<button type="button" class="mdl-button mdl-js-button mdl-button--colored mdl-button--raised {$prefix}continue">{msg desc="Continue to the next step"}Continue{/msg}</button>
132138
{/if}
133139
</div>
134140
</div>

static_files/resources/tour/basic/blocks/cwc.cwct

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
}, {
6262
"title": "Tooltips",
6363
"attachTo": {
64-
"element": "#cwc-status-button-body",
64+
"element": "#cwc-preview-status-button-body",
6565
"on": "right"
6666
},
6767
"text": "Coding with Chrome has lots of buttons. Hover your mouse over one to see what it does."
@@ -94,4 +94,4 @@
9494
},
9595
"mode": "basic_blockly",
9696
"ui": "blockly"
97-
}
97+
}

static_files/tutorials/basic/blocks/blockly.cwct/cwct.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"validate": {
4444
"type": "function",
4545
"___TEMPLATE___:value": "eng/step1/validate.js"
46-
}
46+
},
47+
"___TEMPLATE___:json:tour": "eng/step1/tour.json"
4748
},
4849
{
4950
"title": "Fill it with blue",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"title": "Add a circle",
4+
"attachTo": {
5+
"element": ".blocklyTreeRowItem_draw",
6+
"on": "right"
7+
},
8+
"text": "Drag a 'Circle' block into the editor"
9+
}
10+
]

0 commit comments

Comments
 (0)