Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
383
gaia/apps/ftu/js/tutorial.js
Executable file
383
gaia/apps/ftu/js/tutorial.js
Executable file
|
|
@ -0,0 +1,383 @@
|
|||
/* global ScreenLayout, Promise,
|
||||
Utils, FinishScreen, LazyLoader, TutorialUtils */
|
||||
/* exported Tutorial */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
// default layout
|
||||
var currentLayout = 'tiny';
|
||||
|
||||
// Most used DOM elements
|
||||
var dom = {};
|
||||
|
||||
/**
|
||||
* Manages and controls the configuration, content and state of the tutorial
|
||||
* @module Tutorial
|
||||
*/
|
||||
var elementIDs = [
|
||||
'tutorial',
|
||||
'tutorial-step-title',
|
||||
'tutorial-step-media',
|
||||
'tutorial-step-image',
|
||||
'tutorial-step-video',
|
||||
'forward-tutorial',
|
||||
'back-tutorial'
|
||||
];
|
||||
|
||||
var Tutorial = {
|
||||
// A configuration object.
|
||||
config: null,
|
||||
|
||||
// Config for the current tutorial steps
|
||||
_stepsConfig: {},
|
||||
|
||||
// Which tutorial steps to use
|
||||
_stepsKey: '',
|
||||
|
||||
// Keeps track of the current step
|
||||
_currentStep: 1,
|
||||
|
||||
// Track initialized state of tutorial
|
||||
_initialized: false,
|
||||
|
||||
/**
|
||||
* Initialize the tutorial. This is async as a config file must be loaded.
|
||||
* A Sequence (array of sync or async functions) is used to manage the init
|
||||
* tasks.
|
||||
* When complete, the tutorial is ready to be shown via the 'start' method
|
||||
*
|
||||
* @param {String} stepsKey a key into the tutorial config object, i
|
||||
* i.e. a version or version delta like 1.3..2.0
|
||||
* @param {Function} onLoaded optional callback for when init is complete
|
||||
* @memberof Tutorial
|
||||
*/
|
||||
init: function(stepsKey, onLoaded) {
|
||||
// init is async
|
||||
// need to load config, then load the first step and its assets.
|
||||
if (this._initialized || this._initialization) {
|
||||
// init already underway
|
||||
return;
|
||||
}
|
||||
|
||||
var initTasks = this._initialization = new TutorialUtils.Sequence(
|
||||
// config should load or already be loaded.
|
||||
// failure should abort
|
||||
this.loadConfig.bind(this),
|
||||
this._initWithConfig.bind(this, stepsKey)
|
||||
);
|
||||
initTasks.onabort = this._onAbortInitialization.bind(this);
|
||||
initTasks.oncomplete =
|
||||
this._onCompleteInitialization.bind(this, onLoaded);
|
||||
|
||||
// first time here: cache DOM elements
|
||||
elementIDs.forEach(function(name) {
|
||||
dom[Utils.camelCase(name)] = document.getElementById(name);
|
||||
if (!dom[Utils.camelCase(name)]) {
|
||||
console.error('Cache DOM elements: couldnt cache: ' + name);
|
||||
}
|
||||
}, this);
|
||||
|
||||
initTasks.next();
|
||||
|
||||
// watch a ton of video events as context for the tutorial
|
||||
// media loading/playing
|
||||
var mediaEvents = this._mediaEvents = ['abort',
|
||||
'canplay',
|
||||
'canplaythrough',
|
||||
'durationchange',
|
||||
'emptied',
|
||||
'ended',
|
||||
'error',
|
||||
'interruptbegin',
|
||||
'interruptend',
|
||||
'loadeddata',
|
||||
'loadedmetadata',
|
||||
'loadstart',
|
||||
'mozaudioavailable',
|
||||
'pause',
|
||||
'play',
|
||||
'playing',
|
||||
'stalled',
|
||||
'suspend',
|
||||
'waiting'];
|
||||
|
||||
this._debugEventHandler = {
|
||||
handleEvent: function(evt) {
|
||||
var errCodes = {
|
||||
'1': 'MEDIA_ERR_ABORTED',
|
||||
'2': 'MEDIA_ERR_NETWORK',
|
||||
'3': 'MEDIA_ERR_DECODE',
|
||||
'4': 'MEDIA_ERR_SRC_NOT_SUPPORTED'
|
||||
};
|
||||
console.log('TUTORIAL: media-event: '+ evt.type);
|
||||
if (evt.type == 'error') {
|
||||
var errtype = errCodes[evt.target.error.code] || 'unknown';
|
||||
console.log('TUTORIAL: media-event, ' + errtype + ' error');
|
||||
}
|
||||
}
|
||||
};
|
||||
mediaEvents.forEach(name => {
|
||||
dom.tutorialStepVideo.addEventListener(name, this._debugEventHandler);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the tutorial and play the first step
|
||||
* May be called during or after the init process
|
||||
* We defer rasing the 'tutorialinitialized' event until this point
|
||||
* as it signals the tutorial step content is loaded and displayed
|
||||
*
|
||||
* @param {Function} onReady optional callback for when start is complete
|
||||
* @memberof Tutorial
|
||||
*/
|
||||
start: function(onReady) {
|
||||
var sequence;
|
||||
var initInProgress = false;
|
||||
if (this._initialization) {
|
||||
// init still underway, tack steps onto existing sequence
|
||||
sequence = this._initialization;
|
||||
initInProgress = true;
|
||||
} else {
|
||||
// init already complete, create new sequence
|
||||
this._initialization = sequence = new TutorialUtils.Sequence();
|
||||
sequence.onabort = this._onAbortInitialization.bind(this);
|
||||
sequence.oncomplete =
|
||||
this._onCompleteInitialization.bind(this);
|
||||
}
|
||||
sequence.push(function setInitialStep() {
|
||||
// setStep should return promise given by _loadMedia
|
||||
console.log('TUTORIAL: setInitialStep');
|
||||
return this._setStep();
|
||||
}.bind(this));
|
||||
|
||||
sequence.push(function showTutorialAndFinishInit() {
|
||||
// Show the panel
|
||||
dom.tutorial.classList.add('show');
|
||||
// Custom event that can be used to apply (screen reader) visibility
|
||||
// changes.
|
||||
console.log('TUTORIAL: dispatching tutorialinitialized');
|
||||
window.dispatchEvent(new CustomEvent('tutorialinitialized'));
|
||||
});
|
||||
|
||||
if(typeof onReady === 'function') {
|
||||
sequence.push(onReady);
|
||||
}
|
||||
|
||||
if (!initInProgress) {
|
||||
// init done, starting start sequence
|
||||
sequence.next();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Continue initialization once config data is loaded
|
||||
* Called as a part of the init sequence
|
||||
*
|
||||
* @param {String} stepsKey a key into the tutorial config object, i
|
||||
* i.e. a version or version delta like 1.3..2.0
|
||||
* @memberof Tutorial
|
||||
*/
|
||||
_initWithConfig: function(stepsKey) {
|
||||
stepsKey = stepsKey || 'default';
|
||||
this._stepsKey = stepsKey;
|
||||
this._stepsConfig = this.config[stepsKey] || this.config['default'];
|
||||
|
||||
// Add event listeners
|
||||
dom.forwardTutorial.addEventListener('click', this);
|
||||
dom.backTutorial.addEventListener('click', this);
|
||||
|
||||
// Set the first step
|
||||
this._currentStep = 1;
|
||||
},
|
||||
_onAbortInitialization: function() {
|
||||
this._initialization = null;
|
||||
},
|
||||
_onCompleteInitialization: function(onReady) {
|
||||
this._initialization = null;
|
||||
this._initialized = true;
|
||||
if(typeof onReady === 'function') {
|
||||
onReady();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Advance the tutorial to the given step (first step if no value given)
|
||||
*
|
||||
* @param {Number} value number of the step to show (1-based)
|
||||
* @memberof Tutorial
|
||||
*/
|
||||
_setStep: function (value) {
|
||||
// If value is bigger than the max, show finish screen
|
||||
value = (typeof value === 'number') ? value : this._currentStep;
|
||||
var stepIndex = value - 1;
|
||||
if (stepIndex >= this._stepsConfig.steps.length) {
|
||||
return Promise.resolve().then(function() {
|
||||
Tutorial.done();
|
||||
});
|
||||
}
|
||||
|
||||
var stepData = this._stepsConfig.steps[stepIndex];
|
||||
if (!stepData) {
|
||||
return Promise.reject('No data for step: ' + value);
|
||||
}
|
||||
// Set the step
|
||||
dom.tutorial.dataset.step = this._currentStep;
|
||||
|
||||
// Internationalize
|
||||
document.l10n.setAttributes(
|
||||
dom.tutorialStepTitle,
|
||||
stepData.l10nKey
|
||||
);
|
||||
|
||||
// Update the image/video
|
||||
var imgElement = dom.tutorialStepImage,
|
||||
videoElement = dom.tutorialStepVideo;
|
||||
|
||||
var stepPromise;
|
||||
if (stepData.video) {
|
||||
stepPromise = TutorialUtils.getBestAssetForDirection(stepData.video)
|
||||
.then((bestSrc) => {
|
||||
return TutorialUtils.loadAndPlayMedia(videoElement, bestSrc);
|
||||
});
|
||||
videoElement.hidden = false;
|
||||
imgElement.hidden = true;
|
||||
} else {
|
||||
imgElement.hidden = false;
|
||||
stepPromise = TutorialUtils.loadMedia(imgElement, stepData.image);
|
||||
imgElement.hidden = false;
|
||||
videoElement.hidden = true;
|
||||
}
|
||||
return stepPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* DOM Event handler
|
||||
*
|
||||
* @param {DOMEvent} evt Event object
|
||||
* @memberof Tutorial
|
||||
*/
|
||||
handleEvent: function(evt) {
|
||||
if (evt.type === 'click') {
|
||||
switch(evt.target) {
|
||||
case dom.forwardTutorial:
|
||||
this.next(evt);
|
||||
break;
|
||||
case dom.backTutorial:
|
||||
this.back(evt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Advance to the next step in the tutorial
|
||||
* @memberof Tutorial
|
||||
*/
|
||||
next: function() {
|
||||
return this._setStep(++this._currentStep);
|
||||
},
|
||||
|
||||
/**
|
||||
* Go back to the previous step in the tutorial
|
||||
* @memberof Tutorial
|
||||
*/
|
||||
back: function() {
|
||||
return this._setStep(--this._currentStep);
|
||||
},
|
||||
|
||||
/**
|
||||
* Tutorial complete
|
||||
* @memberof Tutorial
|
||||
*/
|
||||
done: function() {
|
||||
var isUpgrade = this._stepsKey && this._stepsKey !== 'default';
|
||||
FinishScreen.init(isUpgrade);
|
||||
dom.tutorial.classList.remove('show');
|
||||
dom.tutorialStepVideo.removeAttribute('src');
|
||||
dom.tutorialStepImage.removeAttribute('src');
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the config with steps and associated resources for the tutorial
|
||||
* We have different config files for each screen category:
|
||||
* phone is 'tiny', tablet is 'large'
|
||||
* and corresponding tiny.json, large.json
|
||||
*
|
||||
* @returns {Promise}
|
||||
* @memberof Tutorial
|
||||
*/
|
||||
loadConfig: function() {
|
||||
if (!this._configPromise) {
|
||||
// Update the value of the layout if needed
|
||||
// 'ScreenLayout' give us 4 different values
|
||||
// tiny: '(max-width: 767px)',
|
||||
// small: '(min-width: 768px) and (max-width: 991px)',
|
||||
// medium: '(min-width: 992px) and (max-width: 1200px)',
|
||||
// large: '(min-width: 1201px)',
|
||||
//
|
||||
// Currently we are taking into account only 'tiny', and we are
|
||||
// going to consider 'tablet' as 'large'. If we want to add more
|
||||
// or specific features for 'small' & 'medium', we should add more
|
||||
// logic here.
|
||||
|
||||
currentLayout = ScreenLayout.getCurrentLayout() === 'tiny' ?
|
||||
'tiny' : 'large';
|
||||
|
||||
var configUrl = '/config/' + currentLayout + '.json';
|
||||
|
||||
this._configPromise = LazyLoader.getJSON(configUrl)
|
||||
.then(function(json) {
|
||||
Tutorial.config = json;
|
||||
return Tutorial.config;
|
||||
}, function() {
|
||||
return new Error('Tutorial config failed to load from: ' + configUrl);
|
||||
});
|
||||
}
|
||||
return this._configPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Test helper to reset the tutorial to its pre-initialized state
|
||||
* to allow init to be called again
|
||||
* @memberof Tutorial
|
||||
*/
|
||||
reset: function() {
|
||||
var resetPromise = Promise.resolve();
|
||||
if (dom.tutorialStepVideo) {
|
||||
this._mediaEvents.forEach(name => {
|
||||
dom.tutorialStepVideo.removeEventListener(
|
||||
name,
|
||||
this._debugEventHandler
|
||||
);
|
||||
});
|
||||
dom.tutorialStepVideo.hidden = true;
|
||||
if (dom.tutorialStepVideo.src) {
|
||||
resetPromise = new Promise((resolve, reject) => {
|
||||
dom.tutorialStepVideo.addEventListener('emptied', () => {
|
||||
resolve();
|
||||
});
|
||||
dom.tutorialStepVideo.removeAttribute('src');
|
||||
dom.tutorialStepVideo.load();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this._initialization) {
|
||||
this._initialization.abort();
|
||||
this._initialization = null;
|
||||
}
|
||||
this._configPromise = null;
|
||||
this._currentStep = 1;
|
||||
this._stepsConfig = this.config = null;
|
||||
if (this._initialized) {
|
||||
dom.tutorial.classList.remove('show');
|
||||
this._initialized = false;
|
||||
}
|
||||
document.getElementById('tutorial').classList.remove('show');
|
||||
return resetPromise;
|
||||
}
|
||||
};
|
||||
|
||||
exports.Tutorial = Tutorial;
|
||||
|
||||
})(this);
|
||||
Loading…
Reference in a new issue