in webapp/src/tutorial.tsx [345:659]
ariaLabel: lf("Launch Immersive Reader"),
title: lf("Launch Immersive Reader")
})
}
actions.push({
label: hideIteration && flyoutOnly ? lf("Start") : lf("Ok"),
onclick: onClick,
icon: 'check',
className: 'green'
});
const classes = this.props.parent.createModalClasses("hintdialog");
return <sui.Modal isOpen={visible} className={classes}
closeIcon={false} header={tutorialName} buttons={actions}
onClose={onClick} dimmer={true} longer={true}
closeOnDimmerClick closeOnDocumentClick closeOnEscape>
<md.MarkedContent markdown={fullText} parent={this.props.parent} />
</sui.Modal>
}
}
}
interface TutorialCardState {
showHint?: boolean;
showSeeMore?: boolean;
showTutorialValidationMessage?: boolean;
}
interface TutorialCardProps extends ISettingsProps {
pokeUser?: boolean;
}
export class TutorialCard extends data.Component<TutorialCardProps, TutorialCardState> {
private prevStep: number;
private cardHeight: number;
private resizeDebouncer: () => void;
public focusInitialized: boolean;
constructor(props: ISettingsProps) {
super(props);
const options = this.props.parent.state.tutorialOptions;
this.prevStep = options.tutorialStep;
this.state = {
showSeeMore: false,
showHint: options.tutorialStepInfo[this.prevStep].showHint,
showTutorialValidationMessage: false
}
this.toggleHint = this.toggleHint.bind(this);
this.closeHint = this.closeHint.bind(this);
this.hintOnClick = this.hintOnClick.bind(this);
this.closeLightbox = this.closeLightbox.bind(this);
this.tutorialCardKeyDown = this.tutorialCardKeyDown.bind(this);
this.okButtonKeyDown = this.okButtonKeyDown.bind(this);
this.previousTutorialStep = this.previousTutorialStep.bind(this);
this.nextTutorialStep = this.nextTutorialStep.bind(this);
this.finishTutorial = this.finishTutorial.bind(this);
this.toggleExpanded = this.toggleExpanded.bind(this);
this.onMarkdownDidRender = this.onMarkdownDidRender.bind(this);
this.handleResize = this.handleResize.bind(this);
this.showTutorialValidationMessageOnClick = this.showTutorialValidationMessageOnClick.bind(this);
this.closeTutorialValidationMessage = this.closeTutorialValidationMessage.bind(this);
this.doubleClickedNextStep = this.doubleClickedNextStep.bind(this);
this.validationTelemetry = this.validationTelemetry.bind(this);
}
previousTutorialStep() {
this.showHint(false); // close hint on new tutorial step
let options = this.props.parent.state.tutorialOptions;
const currentStep = options.tutorialStep;
const previousStep = currentStep - 1;
options.tutorialStep = previousStep;
pxt.tickEvent(`tutorial.previous`, { tutorial: options.tutorial, step: previousStep }, { interactiveConsent: true });
this.props.parent.setTutorialStep(previousStep);
this.setState({ showTutorialValidationMessage: false });
}
nextTutorialStep() {
this.showHint(false); // close hint on new tutorial step
let options = this.props.parent.state.tutorialOptions;
const currentStep = options.tutorialStep;
const nextStep = currentStep + 1;
options.tutorialStep = nextStep;
pxt.tickEvent(`tutorial.next`, { tutorial: options.tutorial, step: nextStep }, { interactiveConsent: true });
this.props.parent.setTutorialStep(nextStep);
const tutorialCodeValidationIsOn = options.metadata.tutorialCodeValidation;
if (tutorialCodeValidationIsOn && this.state.showTutorialValidationMessage) { // disables tutorial validation pop-up if next buttion is clicked
this.setState({ showTutorialValidationMessage: false });
}
}
doubleClickedNextStep() {
this.validationTelemetry('next');
this.nextTutorialStep();
}
finishTutorial() {
this.closeLightbox();
this.removeHintOnClick();
this.props.parent.completeTutorialAsync();
}
private closeLightboxOnEscape = (e: KeyboardEvent) => {
const charCode = core.keyCodeFromEvent(e);
if (charCode === 27) {
this.closeLightbox();
}
}
private closeLightbox() {
sounds.tutorialNext();
document.documentElement.removeEventListener("keydown", this.closeLightboxOnEscape);
// Hide lightbox
this.props.parent.hideLightbox();
}
UNSAFE_componentWillUpdate() {
document.documentElement.addEventListener("keydown", this.closeLightboxOnEscape);
}
private tutorialCardKeyDown(e: KeyboardEvent) {
const charCode = core.keyCodeFromEvent(e);
if (charCode == core.TAB_KEY) {
e.preventDefault();
const tutorialOkRef = this.refs["tutorialok"] as sui.Button;
const okButton = ReactDOM.findDOMNode(tutorialOkRef) as HTMLElement;
okButton.focus();
}
}
private okButtonKeyDown(e: KeyboardEvent) {
const charCode = core.keyCodeFromEvent(e);
if (charCode == core.TAB_KEY) {
e.preventDefault();
const tutorialCard = this.refs['tutorialmessage'] as HTMLElement;
tutorialCard.focus();
}
}
private lastStep = -1;
componentDidUpdate(prevProps: ISettingsProps, prevState: TutorialCardState) {
const options = this.props.parent.state.tutorialOptions;
const tutorialCard = this.refs['tutorialmessage'] as HTMLElement;
const step = this.props.parent.state.tutorialOptions.tutorialStep;
if (step != this.lastStep) {
const animationClasses = `fade ${step < this.lastStep ? "right" : "left"} in visible transition animating`;
tutorialCard.style.animationDuration = '500ms';
this.lastStep = step;
pxsim.U.addClass(tutorialCard, animationClasses);
pxt.Util.delay(500)
.then(() => pxsim.U.removeClass(tutorialCard, animationClasses));
}
if (this.prevStep != step) {
this.setShowSeeMore(options.autoexpandStep);
this.prevStep = step;
// on "new step", sync tutorial card state. used when exiting the modal, since that bypasses the react lifecycle
this.setState({ showHint: options.tutorialStepInfo[step].showDialog || options.tutorialStepInfo[step].showHint })
}
}
private handleResize() {
const options = this.props.parent.state.tutorialOptions;
this.setShowSeeMore(options.autoexpandStep);
}
componentDidMount() {
this.setShowSeeMore(this.props.parent.state.tutorialOptions.autoexpandStep);
this.resizeDebouncer = pxt.Util.debounce(this.handleResize, 500);
window.addEventListener('resize', this.resizeDebouncer);
}
onMarkdownDidRender() {
this.setShowSeeMore(this.props.parent.state.tutorialOptions.autoexpandStep);
}
componentWillUnmount() {
// Clear the markdown cache when we unmount
md.MarkedContent.clearBlockSnippetCache();
this.lastStep = -1;
// Clear any existing timers
this.props.parent.stopPokeUserActivity();
this.removeHintOnClick();
window.removeEventListener('resize', this.resizeDebouncer);
}
private removeHintOnClick() {
// cleanup hintOnClick
document.removeEventListener('click', this.closeHint);
}
toggleExpanded(ev: React.MouseEvent<HTMLDivElement>) {
ev.stopPropagation();
ev.preventDefault();
const options = this.props.parent.state.tutorialOptions;
const { tutorialStepExpanded } = options;
this.props.parent.setTutorialInstructionsExpanded(!tutorialStepExpanded);
return false;
}
private hasHint() {
const options = this.props.parent.state.tutorialOptions;
const { tutorialReady, tutorialStepInfo, tutorialStep } = options;
if (!tutorialReady) return false;
return !!tutorialStepInfo[tutorialStep].hintContentMd
|| tutorialStepInfo[tutorialStep].showDialog;
}
private hintOnClick(evt?: any) {
const options = this.props.parent.state.tutorialOptions;
if (!options) {
pxt.reportError("tutorial", "leaking hintonclick");
return;
}
if (evt) evt.stopPropagation();
const { tutorialStepInfo, tutorialStep } = options;
const step = tutorialStepInfo[tutorialStep];
const showDialog = tutorialStep < tutorialStepInfo.length - 1 && step && !!step.showDialog;
this.props.parent.clearUserPoke();
if (!showDialog) {
this.toggleHint();
}
}
private showTutorialValidationMessageOnClick(evt?: any) {
this.setState({ showTutorialValidationMessage: true });
}
private expandedHintOnClick(evt?: any) {
evt.stopPropagation();
}
private setShowSeeMore(autoexpand?: boolean) {
// compare scrollHeight of inner text with height of card to determine showSeeMore
const tutorialCard = this.refs['tutorialmessage'] as HTMLElement;
let show = false;
if (tutorialCard && tutorialCard.firstElementChild && tutorialCard.firstElementChild.firstElementChild) {
show = tutorialCard.clientHeight <= tutorialCard.firstElementChild.firstElementChild.scrollHeight;
if (show) {
this.cardHeight = tutorialCard.firstElementChild.firstElementChild.scrollHeight;
if (autoexpand) this.props.parent.setTutorialInstructionsExpanded(true);
}
}
this.setState({ showSeeMore: show });
this.props.parent.setEditorOffset();
}
getCardHeight() {
return this.cardHeight;
}
getExpandedCardStyle(prop: string) {
return { [prop]: `calc(${this.getCardHeight()}px + 2rem)` }
}
toggleHint(showFullText?: boolean) {
this.showHint(!this.state.showHint, showFullText);
}
closeHint(evt?: any) {
this.showHint(false);
}
showHint(visible: boolean, showFullText?: boolean) {
this.removeHintOnClick();
this.closeLightbox();
if (!this.hasHint()) return;
const th = this.refs["tutorialhint"] as TutorialHint;
if (!th) return;
const currentStep = this.props.parent.state.tutorialOptions.tutorialStep;
if (!visible) {
if (th.elementRef) th.elementRef.removeEventListener('click', this.expandedHintOnClick);
this.setState({ showHint: false });
this.props.parent.pokeUserActivity();
} else {
if (th.elementRef) th.elementRef.addEventListener('click', this.expandedHintOnClick);
this.setState({ showHint: true });
this.props.parent.stopPokeUserActivity();
const options = this.props.parent.state.tutorialOptions;
if (!options.tutorialStepInfo[options.tutorialStep].showDialog)
document.addEventListener('click', this.closeHint); // add close listener if not modal
pxt.tickEvent(`tutorial.showhint`, { tutorial: options.tutorial, step: options.tutorialStep });
this.props.parent.setHintSeen(currentStep);
}
th.showHint(visible, showFullText);
if (visible) {
this.setState({ showTutorialValidationMessage: false });
}
}
closeTutorialValidationMessage() {
this.setState({ showTutorialValidationMessage: false });
}
isCodeValidated(rules: pxt.tutorial.TutorialRuleStatus[]) {
if (rules != undefined) {
for (let i = 0; i < rules.length; i++) {
if (rules[i].ruleTurnOn && !rules[i].ruleStatus) {