Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
179 lines
6 KiB
JavaScript
Executable file
179 lines
6 KiB
JavaScript
Executable file
(function(exports) {
|
|
'use strict';
|
|
|
|
var TutorialUtils = {
|
|
/**
|
|
* Helper function to load images and video
|
|
* @param {DOMNode} mediaElement video or image to assign new src to
|
|
* @param {String} src URL for video/image resource
|
|
* @returns {Promise}
|
|
*/
|
|
loadMedia: function (mediaElement, src) {
|
|
console.log('loadMedia, got mediaElement: ', mediaElement);
|
|
var isVideo = (mediaElement.nodeName === 'VIDEO');
|
|
console.log('TutorialUtils.loadMedia, isVideo: ',
|
|
isVideo, mediaElement.nodeName);
|
|
return new Promise(function(resolve, reject) {
|
|
function onMediaLoadOrError(evt) {
|
|
console.log('TutorialUtils.loadMedia, onMediaLoadOrError for ' +
|
|
'evt.type: ' + (evt && evt.type));
|
|
evt.target.removeEventListener('error', onMediaLoadOrError);
|
|
if (isVideo) {
|
|
evt.target.removeEventListener('canplay', onMediaLoadOrError);
|
|
evt.target.removeEventListener('abort', onMediaLoadOrError);
|
|
} else {
|
|
evt.target.removeEventListener('load', onMediaLoadOrError);
|
|
}
|
|
// Dont block progress on failure to load media
|
|
if (evt.type === 'error') {
|
|
console.error('Failed to load tutorial media: ' + src);
|
|
} else if (evt.type === 'abort') {
|
|
console.error('Loading of tutorial media aborted: ' + src);
|
|
}
|
|
resolve(evt);
|
|
}
|
|
function onVideoUnloaded(evt) {
|
|
console.log('TutorialUtils.loadMedia, onVideoUnloaded for evt.type: ' +
|
|
(evt && evt.type));
|
|
mediaElement.removeEventListener('emptied', onVideoUnloaded);
|
|
mediaElement.addEventListener('canplay', onMediaLoadOrError);
|
|
mediaElement.addEventListener('abort', onMediaLoadOrError);
|
|
mediaElement.addEventListener('error', onMediaLoadOrError);
|
|
console.log('TutorialUtils.loadMedia, onVideoUnloaded, ' +
|
|
'assigning src: ' + src);
|
|
mediaElement.src = src;
|
|
mediaElement.load();
|
|
}
|
|
if (isVideo) {
|
|
// must unload video and force load before switching to new source
|
|
if (mediaElement.src) {
|
|
console.log('TutorialUtils.loadMedia, removing src attribute');
|
|
mediaElement.removeAttribute('src');
|
|
mediaElement.addEventListener('emptied', onVideoUnloaded, false);
|
|
console.log('TutorialUtils.loadMedia, calling load');
|
|
mediaElement.load();
|
|
} else {
|
|
console.log('TutorialUtils.loadMedia, no src, ' +
|
|
'just calling onVideoUnloaded');
|
|
onVideoUnloaded();
|
|
}
|
|
} else {
|
|
console.log('TutorialUtils.loadMedia, not a video, ' +
|
|
'listen for load/error only');
|
|
mediaElement.addEventListener('load', onMediaLoadOrError, false);
|
|
mediaElement.addEventListener('error', onMediaLoadOrError, false);
|
|
mediaElement.src = src;
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Convenience to load and then play video
|
|
* @param {DOMNode} mediaElement video or image to assign new src to
|
|
* @param {String} src URL for video/image resource
|
|
* @returns {Promise}
|
|
*/
|
|
loadAndPlayMedia: function(mediaElement, src) {
|
|
return this.loadMedia(mediaElement, src).then((evt) => {
|
|
if (evt.type !== 'error' && mediaElement.nodeName === 'VIDEO') {
|
|
mediaElement.play();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Helper to check (via HEAD request) a given resource exists
|
|
* The returned promise resolves with a true|false value
|
|
*
|
|
* @param {String} src URL for video/image resource
|
|
* @returns {Promise}
|
|
*/
|
|
fileExists: function (src) {
|
|
return new Promise((resolve) => {
|
|
window.fetch(src, { method: 'HEAD' }).then((response) => {
|
|
resolve(response.ok);
|
|
}).catch(() => {
|
|
resolve(false);
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Helper to get best-match resource url for the current document.dir
|
|
* @param {String} src URL for video/image resource
|
|
* @returns {Promise}
|
|
*/
|
|
getBestAssetForDirection: function(src) {
|
|
var dir = document.dir || 'ltr';
|
|
var re =
|
|
/(-rtl|ltr)?\.(png|gif|jpg|webm|mp4|m4v|ogg|ogv)$/;
|
|
var directionSrc = src.replace(re, '-'+dir+'.$2');
|
|
return this.fileExists(directionSrc).then((result) => {
|
|
return result ? directionSrc : src;
|
|
}).catch((e) => {
|
|
console.log('Exception from getBestAssetForDirection: ', e);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Private helper class to manage a series of sync or async functions
|
|
*
|
|
* The array may be manipulated using standard array methods while the
|
|
* sequence runs. The sequence completes when there are no more functions or
|
|
* an exception is raised.
|
|
* At the end of the sequence, any 'oncomplete' assigned will be called with
|
|
* the return value from the last function
|
|
* Functions may return a 'thenable' to indicate async return
|
|
* Exceptions will be passed into the oncomplete function
|
|
* A Sequence may be cleanly aborted by calling abort() - no callbacks will
|
|
* be fired
|
|
* @class Sequence
|
|
*/
|
|
Sequence: function() {
|
|
var sequence = Array.slice(arguments);
|
|
var aborted = false;
|
|
sequence.abort = function() {
|
|
aborted = true;
|
|
this.length = 0;
|
|
if (typeof this.onabort === 'function') {
|
|
this.onabort();
|
|
}
|
|
};
|
|
sequence.complete = function(result) {
|
|
if(!aborted && typeof this.oncomplete === 'function') {
|
|
this.oncomplete(result);
|
|
}
|
|
};
|
|
sequence.fail = function(reason) {
|
|
this.complete(reason);
|
|
};
|
|
sequence.next = function(previousTaskResult) {
|
|
var result, exception;
|
|
if (aborted) {
|
|
return;
|
|
}
|
|
var task = this.shift();
|
|
if (task) {
|
|
try {
|
|
result = task.apply(null, arguments);
|
|
} catch(e) {
|
|
exception = e;
|
|
}
|
|
if (exception) {
|
|
this.fail(exception);
|
|
} else if (result && typeof result.then === 'function') {
|
|
result.then(this.next.bind(this), this.fail.bind(this));
|
|
} else {
|
|
this.next(result);
|
|
}
|
|
} else {
|
|
this.complete(previousTaskResult);
|
|
}
|
|
};
|
|
return sequence;
|
|
}
|
|
};
|
|
|
|
exports.TutorialUtils = TutorialUtils;
|
|
|
|
})(window);
|