mirror of
https://github.com/scratchfoundation/scratchjr.git
synced 2024-11-24 16:17:55 -05:00
add support for custom options questions at app start; refactor index.js
This commit is contained in:
parent
d73abf18b4
commit
62a651369f
6 changed files with 415 additions and 104 deletions
|
@ -344,3 +344,43 @@ div.frame {
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
.optionsInstruction {
|
||||
position: absolute;
|
||||
width: 96%;
|
||||
top: 0;
|
||||
padding: 2%;
|
||||
color: #ffdd44;
|
||||
font-size: 2.75rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px 4px #333333;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.optionsInstruction.hide {display: none;}
|
||||
.optionsInstruction.show {display: block;}
|
||||
|
||||
.optionsList {
|
||||
position: absolute;
|
||||
width: 92%;
|
||||
height: 86%;
|
||||
padding: 4%;
|
||||
top: 6%;
|
||||
column-count: 3;
|
||||
column-fill: auto; /* use first column up first */
|
||||
color: white;
|
||||
font-size: 2.25rem;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
text-shadow: 1px 1px 4px #333333;
|
||||
list-style-type: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.optionsList.hide {display: none;}
|
||||
.optionsList.show {display: block;}
|
||||
|
||||
.optionsList > div {
|
||||
display: block;
|
||||
padding: .25em;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,11 @@
|
|||
<div class="usageNoanswer hide" id="usageNoanswer">
|
||||
<div class="usageNoanswerText" id="usageNoanswerText"></div>
|
||||
</div>
|
||||
|
||||
<div class="optionsInstruction hide" id="optionsInstruction">
|
||||
</div>
|
||||
<div class="optionsList hide" id="optionsList">
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {preprocessAndLoadCss} from '../utils/lib';
|
||||
import Localization from '../utils/Localization';
|
||||
import AppUsage from '../utils/AppUsage';
|
||||
import InitialOptions from '../utils/InitialOptions';
|
||||
import OS from '../tablet/OS';
|
||||
import IO from '../tablet/IO';
|
||||
import MediaLib from '../tablet/MediaLib';
|
||||
|
@ -110,6 +110,6 @@ window.onload = () => {
|
|||
});
|
||||
});
|
||||
// Initialize currentUsage data
|
||||
AppUsage.initUsage();
|
||||
InitialOptions.initWithSettings(window.Settings.initialOptions);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,17 +3,27 @@ import {gn, getUrlVars, isAndroid, isiOS} from '../utils/lib';
|
|||
import OS from '../tablet/OS';
|
||||
import UI from '../editor/ui/UI';
|
||||
import Localization from '../utils/Localization';
|
||||
import AppUsage from '../utils/AppUsage';
|
||||
import InitialOptions from '../utils/InitialOptions';
|
||||
|
||||
/*
|
||||
When this code starts up, there are several scenarios:
|
||||
* the app was already running: "AlreadyRunning"
|
||||
* the app was not already running, but has been opened before: "NewSession"
|
||||
* the app has never been opened before: "FirstTimeEver"
|
||||
|
||||
*/
|
||||
|
||||
let alreadyStartedQuestions = false;
|
||||
|
||||
export function indexMain () {
|
||||
gn('gettings').ontouchend = indexGettingstarted;
|
||||
gn('startcode').ontouchend = indexGohome;
|
||||
ScratchAudio.init();
|
||||
var urlvars = getUrlVars();
|
||||
if (urlvars.back) {
|
||||
indexLoadOptions();
|
||||
if (urlvars.back && InitialOptions.allQuestionsAnswered()) {
|
||||
indexLoadStart();
|
||||
} else {
|
||||
indexFirstTime();
|
||||
indexNewSession();
|
||||
}
|
||||
|
||||
if (window.Settings.edition == 'PBS') {
|
||||
|
@ -36,7 +46,28 @@ export function indexMain () {
|
|||
}, 250);
|
||||
}
|
||||
|
||||
function indexFirstTime () {
|
||||
function startQuestionsIfNotAlreadyStarted () {
|
||||
if (!alreadyStartedQuestions) {
|
||||
alreadyStartedQuestions = true;
|
||||
indexAskRemainingQuestions();
|
||||
}
|
||||
window.removeEventListener('touchend', startQuestionsIfNotAlreadyStarted, false);
|
||||
}
|
||||
|
||||
function indexNewSession () {
|
||||
showSplash();
|
||||
OS.askpermission(); // ask for sound recording
|
||||
setTimeout(function () {
|
||||
OS.hidesplash(addTouchListener);
|
||||
}, 500);
|
||||
// may be necessary to wait for a touch in some environments
|
||||
function addTouchListener () {
|
||||
window.addEventListener('touchend', startQuestionsIfNotAlreadyStarted, false);
|
||||
}
|
||||
setTimeout(startQuestionsIfNotAlreadyStarted, 2000);
|
||||
}
|
||||
|
||||
function showSplash () {
|
||||
gn('authors').className = 'credits show';
|
||||
gn('authorsText').className = 'creditsText show';
|
||||
if (window.Settings.edition == 'PBS') {
|
||||
|
@ -49,29 +80,9 @@ function indexFirstTime () {
|
|||
gn('blueguy').className = 'blue show';
|
||||
gn('redguy').className = 'red show';
|
||||
}
|
||||
OS.askpermission(); // ask for sound recording
|
||||
setTimeout(function () {
|
||||
OS.hidesplash(doit);
|
||||
}, 500);
|
||||
function doit () {
|
||||
window.ontouchend = function () {
|
||||
indexLoadOptions();
|
||||
};
|
||||
}
|
||||
setTimeout(function () {
|
||||
indexLoadOptions();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function indexLoadOptions () {
|
||||
if (window.Settings.edition != 'PBS' && AppUsage.askForUsage()) {
|
||||
indexLoadUsage();
|
||||
} else {
|
||||
indexLoadStart();
|
||||
}
|
||||
}
|
||||
|
||||
function indexLoadStart (afterUsage) {
|
||||
function indexHideSplash () {
|
||||
gn('authors').className = 'credits hide';
|
||||
gn('authorsText').className = 'creditsText hide';
|
||||
|
||||
|
@ -84,18 +95,11 @@ function indexLoadStart (afterUsage) {
|
|||
gn('blueguy').className = 'blue hide';
|
||||
gn('redguy').className = 'red hide';
|
||||
gn('gear').className = 'gear show';
|
||||
|
||||
if (afterUsage) {
|
||||
gn('catface').className = 'catface show';
|
||||
gn('jrlogo').className = 'jrlogo show';
|
||||
gn('usageQuestion').className = 'usageQuestion hide';
|
||||
gn('usageSchool').className = 'usageSchool hide';
|
||||
gn('usageHome').className = 'usageHome hide';
|
||||
gn('usageOther').className = 'usageOther hide';
|
||||
gn('usageNoanswer').className = 'usageNoanswer hide';
|
||||
}
|
||||
OS.setAnalyticsPlacePref(AppUsage.currentUsage);
|
||||
}
|
||||
}
|
||||
|
||||
function indexLoadStart () {
|
||||
indexHideSplash();
|
||||
gn('gettings').className = 'gettings show';
|
||||
gn('startcode').className = 'startcode show';
|
||||
document.ontouchmove = function (e) {
|
||||
|
@ -106,7 +110,17 @@ function indexLoadStart (afterUsage) {
|
|||
}
|
||||
}
|
||||
|
||||
function indexLoadUsage () {
|
||||
function indexAskRemainingQuestions () {
|
||||
indexHideSplash();
|
||||
var nextQuestionKey = InitialOptions.nextUnansweredQuestion();
|
||||
if (nextQuestionKey) {
|
||||
indexShowQuestion(nextQuestionKey);
|
||||
} else { // done with questions
|
||||
indexLoadStart();
|
||||
}
|
||||
}
|
||||
|
||||
function indexAskPlace () {
|
||||
gn('authors').className = 'credits show';
|
||||
gn('authorsText').className = 'creditsText hide';
|
||||
gn('purpleguy').className = 'purple hide';
|
||||
|
@ -126,10 +140,137 @@ function indexLoadUsage () {
|
|||
gn('usageHome').className = 'usageHome show';
|
||||
gn('usageOther').className = 'usageOther show';
|
||||
gn('usageNoanswer').className = 'usageNoanswer show';
|
||||
gn('usageSchool').ontouchend = indexSetUsage;
|
||||
gn('usageHome').ontouchend = indexSetUsage;
|
||||
gn('usageOther').ontouchend = indexSetUsage;
|
||||
gn('usageNoanswer').ontouchend = indexSetUsage;
|
||||
gn('usageSchool').ontouchend = indexSetPlace;
|
||||
gn('usageHome').ontouchend = indexSetPlace;
|
||||
gn('usageOther').ontouchend = indexSetPlace;
|
||||
gn('usageNoanswer').ontouchend = indexSetPlace;
|
||||
}
|
||||
|
||||
function indexSetPlace (e) {
|
||||
var usageText = '';
|
||||
|
||||
switch (e.target.parentElement.id) {
|
||||
case 'usageSchool':
|
||||
usageText = 'school';
|
||||
break;
|
||||
case 'usageHome':
|
||||
usageText = 'home';
|
||||
break;
|
||||
case 'usageOther':
|
||||
usageText = 'other';
|
||||
break;
|
||||
case 'usageNoanswer':
|
||||
default:
|
||||
usageText = 'noanswer';
|
||||
break;
|
||||
}
|
||||
// Send one-time analytics event about usage
|
||||
OS.analyticsEvent('lobby', 'scratchjr_usage', usageText);
|
||||
InitialOptions.setValue('place', usageText);
|
||||
// we use 'place_preference' for this particular Firebase pref
|
||||
OS.setAnalyticsPref('place_preference', usageText);
|
||||
ScratchAudio.sndFX('tap.wav');
|
||||
indexHidePlaceQuestion();
|
||||
indexAskRemainingQuestions();
|
||||
}
|
||||
|
||||
function indexHidePlaceQuestion () {
|
||||
gn('catface').className = 'catface show';
|
||||
gn('jrlogo').className = 'jrlogo show';
|
||||
gn('usageQuestion').className = 'usageQuestion hide';
|
||||
gn('usageSchool').className = 'usageSchool hide';
|
||||
gn('usageHome').className = 'usageHome hide';
|
||||
gn('usageOther').className = 'usageOther hide';
|
||||
gn('usageNoanswer').className = 'usageNoanswer hide';
|
||||
}
|
||||
|
||||
function optionTouched (elem) {
|
||||
var key = elem.target.getAttribute('data-key');
|
||||
var value = elem.target.getAttribute('data-value');
|
||||
// sometimes a touch is registered by a child of the relevant parent
|
||||
if (!key && !value) {
|
||||
var parent = elem.target.parentNode;
|
||||
key = parent.getAttribute('data-key');
|
||||
value = parent.getAttribute('data-value');
|
||||
}
|
||||
// if we still don't have a key and value, something is wrong -- just go
|
||||
// to lobby
|
||||
if (!key && !value) {
|
||||
indexLoadStart();
|
||||
return;
|
||||
}
|
||||
// elem.target.style.backgroundColor = 'purple';
|
||||
// if everything is good, register the selection and advance to next screen
|
||||
indexSelectOption(key, value);
|
||||
}
|
||||
|
||||
// show the question for a given settings option key
|
||||
function indexShowQuestion (key) {
|
||||
var optionType = InitialOptions.optionTypeForKey(key);
|
||||
if (optionType === 'place') {
|
||||
indexAskPlace();
|
||||
} else { // custom question
|
||||
var options = InitialOptions.optionsForKey(key);
|
||||
// if we could not find any options, choose 'n/a'
|
||||
if (!options || !options.length) {
|
||||
indexSelectOption(key, 'n/a');
|
||||
return;
|
||||
}
|
||||
// if there's only one option, don't bother asking, just choose it!
|
||||
if (options.length === 1) {
|
||||
indexSelectOption(key, options[0]);
|
||||
return;
|
||||
}
|
||||
// if we got here, there is more than one option...
|
||||
var instructionText = InitialOptions.instructionForKey(key);
|
||||
var instructionElem = document.getElementById('optionsInstruction');
|
||||
instructionElem.appendChild(document.createTextNode(instructionText));
|
||||
gn('optionsInstruction').className = 'optionsInstruction show';
|
||||
|
||||
var optionsListElem = document.getElementById('optionsList');
|
||||
var optionNum = 0;
|
||||
options.forEach(function (option) {
|
||||
var optionElem = document.createElement('div');
|
||||
optionElem.setAttribute('data-key', key);
|
||||
optionElem.setAttribute('data-value', option);
|
||||
optionElem.setAttribute('id', 'option-' + key + '-' + optionNum);
|
||||
optionElem.ontouchend = optionTouched;
|
||||
optionsListElem.appendChild(optionElem);
|
||||
|
||||
switch (optionType) {
|
||||
case 'image':
|
||||
var imgElem = document.createElement('img');
|
||||
imgElem.setAttribute('src', 'svglibrary/' + option);
|
||||
optionElem.appendChild(imgElem);
|
||||
break;
|
||||
case 'text':
|
||||
default:
|
||||
optionElem.appendChild(document.createTextNode(option));
|
||||
break;
|
||||
}
|
||||
optionNum = optionNum + 1;
|
||||
});
|
||||
gn('optionsList').className = 'optionsList show';
|
||||
}
|
||||
}
|
||||
|
||||
// store user selection, and show next question
|
||||
function indexSelectOption (key, val) {
|
||||
InitialOptions.setValue(key, val);
|
||||
OS.setAnalyticsPref(key, val);
|
||||
ScratchAudio.sndFX('tap.wav');
|
||||
|
||||
// clear out old options instruction
|
||||
var instructionElem = document.getElementById('optionsInstruction');
|
||||
instructionElem.innerHTML = '';
|
||||
gn('optionsInstruction').className = 'optionsInstruction hide';
|
||||
// clear out old options content
|
||||
var optionsListElem = document.getElementById('optionsList');
|
||||
optionsListElem.innerHTML = '';
|
||||
gn('optionsList').className = 'optionsList hide';
|
||||
|
||||
// show next question, or advance to start screen
|
||||
indexAskRemainingQuestions();
|
||||
}
|
||||
|
||||
function indexGohome () {
|
||||
|
@ -153,29 +294,6 @@ function indexGettingstarted () {
|
|||
window.location.href = 'gettingstarted.html?place=home';
|
||||
}
|
||||
|
||||
function indexSetUsage (e) {
|
||||
var usageText = '';
|
||||
|
||||
switch (e.target.parentElement.id) {
|
||||
case 'usageSchool':
|
||||
usageText = 'school';
|
||||
break;
|
||||
case 'usageHome':
|
||||
usageText = 'home';
|
||||
break;
|
||||
case 'usageOther':
|
||||
usageText = 'other';
|
||||
break;
|
||||
case 'usageNoanswer':
|
||||
usageText = 'noanswer';
|
||||
break;
|
||||
}
|
||||
// Send one-time analytics event about usage
|
||||
OS.analyticsEvent('lobby', 'scratchjr_usage', usageText);
|
||||
AppUsage.setUsage(usageText);
|
||||
ScratchAudio.sndFX('tap.wav');
|
||||
indexLoadStart(true);
|
||||
}
|
||||
// For PBS KIDS edition only
|
||||
function indexInfo () {
|
||||
ScratchAudio.sndFX('tap.wav');
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import Cookie from './Cookie';
|
||||
|
||||
let currentUsage;
|
||||
|
||||
export default class AppUsage {
|
||||
static get currentUsage () {
|
||||
return currentUsage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize currentUsage for attaching to Analytics events from
|
||||
* the usage cookie if it is set. currentUsage is blank if the cookie is
|
||||
* not set.
|
||||
*/
|
||||
static initUsage () {
|
||||
const usageCookie = Cookie.get('usage');
|
||||
currentUsage = (usageCookie) ? usageCookie : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the App should ask for the usage data (first time launched)
|
||||
* @return {boolean} True if the usage cookie has never been set
|
||||
*/
|
||||
static askForUsage () {
|
||||
var usageCookie = Cookie.get('usage');
|
||||
return usageCookie === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the usage cookie for tracking Analytics Events
|
||||
* @param {string} kind answer from user to the usage survey (home, school, other, noanswer)
|
||||
*/
|
||||
static setUsage (kind) {
|
||||
currentUsage = (kind === '') ? 'noanswer' : kind;
|
||||
Cookie.set('usage', currentUsage);
|
||||
}
|
||||
}
|
185
src/utils/InitialOptions.js
Normal file
185
src/utils/InitialOptions.js
Normal file
|
@ -0,0 +1,185 @@
|
|||
import Cookie from './Cookie';
|
||||
|
||||
let settingsSection = null; // reference to settings.json's "initialOptions" section
|
||||
let currentVals = {}; // locally cached copy of the same key-value settings in cookie
|
||||
// tracks keys for options that have been set during the current session
|
||||
// (useful for questions we want to ask every time the app starts up)
|
||||
let answeredThisSession = {};
|
||||
|
||||
export default class InitialOptions {
|
||||
|
||||
// ****************************************
|
||||
// functions only used from within InitialOptions
|
||||
// ****************************************
|
||||
|
||||
/**
|
||||
* Checks if a cookie is set for a given question
|
||||
* @param {string} key indicates which options question to use
|
||||
*/
|
||||
static hasCookieSet (key) {
|
||||
var usageCookie = Cookie.get(key);
|
||||
return usageCookie !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given question has been answered. If the question needs to be
|
||||
* asked every time the app starts, check that it has been asked this session.
|
||||
* @param {object} question options question object from settings
|
||||
*/
|
||||
static isAnswered (question) {
|
||||
if (!question) return false;
|
||||
if (InitialOptions.hasCookieSet(question.key)) {
|
||||
// if the cookie is set, check if the question needs to be asked
|
||||
// every time (and hasn't been asked yet)
|
||||
if (question.everyTime && !answeredThisSession[question.key]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize currentVals for attaching to Analytics events from
|
||||
* the usage cookie if it is set. currentVals is blank if the cookie is
|
||||
* not set.
|
||||
* @param {string} key indicates which options question to use
|
||||
*/
|
||||
static initKeyFromCookie (key) {
|
||||
const usageCookie = Cookie.get(key);
|
||||
currentVals[key] = (usageCookie) ? usageCookie : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value for a given question and a given field for that question
|
||||
* @param {string} questionKey indicates which options question to use
|
||||
* @param {string} fieldKey indicates which field of that question to use
|
||||
*/
|
||||
static valForKeyAndField (questionKey, fieldKey) {
|
||||
if (!settingsSection || !settingsSection.length) return null;
|
||||
var question = settingsSection.find(function (question) {
|
||||
return (question.key === questionKey);
|
||||
});
|
||||
if (!question || !question[fieldKey]) return null;
|
||||
return question[fieldKey];
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// functions called from outside InitialOptions
|
||||
// ****************************************
|
||||
|
||||
/**
|
||||
* Process settings object from settings.json, and initialize values
|
||||
* using cookie values
|
||||
* @param {object} settingsSectionParam JSON-derived object with entire
|
||||
* initialOptions section from settings
|
||||
*/
|
||||
static initWithSettings (settingsSectionParam) {
|
||||
settingsSection = settingsSectionParam;
|
||||
if (!settingsSection) return;
|
||||
settingsSection.forEach(function (question) {
|
||||
// question is like {key: OPTION_NAME, options: [...]}
|
||||
if (question.firstTime && !question.everyTime) {
|
||||
InitialOptions.initKeyFromCookie(question.key);
|
||||
}
|
||||
});
|
||||
settingsSection.forEach(function (question) {
|
||||
// question is like {key: OPTION_NAME, options: [...]}
|
||||
if (question.everyTime) {
|
||||
InitialOptions.initKeyFromCookie(question.key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instruction for given question
|
||||
* @param {string} key indicates which options question to use
|
||||
*/
|
||||
static instructionForKey (key) {
|
||||
return InitialOptions.valForKeyAndField(key, 'instruction');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get question type for given question
|
||||
* @param {string} key indicates which options question to use
|
||||
*/
|
||||
static optionTypeForKey (key) {
|
||||
var type = InitialOptions.valForKeyAndField(key, 'type');
|
||||
if (!type) {
|
||||
type = 'text'; // default
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of posible value choices for given question.
|
||||
* If question depends on a previous response, use that response to
|
||||
* determine which value choices to show.
|
||||
* @param {string} key indicates which options question to use
|
||||
*/
|
||||
static optionsForKey (key) {
|
||||
if (!settingsSection || !settingsSection.length) return null;
|
||||
var question = settingsSection.find(function (question) {
|
||||
return (question.key === key);
|
||||
});
|
||||
if (!question || !question.values) return null;
|
||||
var valuesKey = 'default';
|
||||
if (question.dependsOn) {
|
||||
var prevSelection = currentVals[question.dependsOn];
|
||||
if (question.values[prevSelection]) valuesKey = prevSelection;
|
||||
}
|
||||
if (!question.values[valuesKey] || !question.values[valuesKey].length) return null;
|
||||
return question.values[valuesKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all of the questions have options set. If any question doesn't
|
||||
* have a cookie value set, or if it needs to be asked every time the app
|
||||
* starts and hasn't yet, return false.
|
||||
*/
|
||||
static allQuestionsAnswered () {
|
||||
if (!settingsSection || !settingsSection.length) return true;
|
||||
settingsSection.forEach(function (question) {
|
||||
if (!InitialOptions.isAnswered(question)) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets array of keys of all questions that still need to be answered
|
||||
* in this app session
|
||||
*/
|
||||
// static unansweredQuestions () {
|
||||
// if (!settingsSection || !settingsSection.length) return [];
|
||||
// return settingsSection.filter(function (question) {
|
||||
// return !InitialOptions.isAnswered(question);
|
||||
// }).map(function (question) {
|
||||
// return question.key;
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* Gets next question that needs to be asked
|
||||
*/
|
||||
static nextUnansweredQuestion () {
|
||||
if (!settingsSection || !settingsSection.length) return [];
|
||||
var nextUnansweredQuestion = settingsSection.find(function (question) {
|
||||
return !InitialOptions.isAnswered(question);
|
||||
});
|
||||
if (nextUnansweredQuestion) return nextUnansweredQuestion.key;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an options value in both cookie, and local object.
|
||||
* @param {string} key indicates which options question this value is for
|
||||
* @param {string} value option chosen by user
|
||||
*/
|
||||
static setValue (key, value) {
|
||||
currentVals[key] = value;
|
||||
Cookie.set(key, currentVals[key]);
|
||||
answeredThisSession[key] = true;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue