diff --git a/docs/TextInput.html b/docs/TextInput.html index ef1c574..24ccf9f 100644 --- a/docs/TextInput.html +++ b/docs/TextInput.html @@ -175,13 +175,13 @@
diff --git a/docs/core_EventManager.js.html b/docs/core_EventManager.js.html index 92da0e8..9b5ee72 100644 --- a/docs/core_EventManager.js.html +++ b/docs/core_EventManager.js.html @@ -699,13 +699,13 @@ export class BuilderKeyResponse
diff --git a/docs/core_GUI.js.html b/docs/core_GUI.js.html index 89e1c6b..eef7a8a 100644 --- a/docs/core_GUI.js.html +++ b/docs/core_GUI.js.html @@ -341,7 +341,7 @@ export class GUI //$.blockUI({ message: "", baseZ: 1}); // show dialog box: - $("#progressbar").progressbar({value: self._progressBarCurrentIncrement}); + $("#progressbar").progressbar({value: self._progressBarCurrentValue}); $("#progressbar").progressbar("option", "max", self._progressBarMax); } @@ -615,15 +615,14 @@ export class GUI { this._psychoJS.logger.debug('signal: ' + util.toString(signal)); - // all resources have been registered: - if (signal.message === ServerManager.Event.RESOURCES_REGISTERED) + // the download of the specified resources has started: + if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCES) { // for each resource, we have a 'downloading resource' and a 'resource downloaded' message: this._progressBarMax = signal.count * 2; $("#progressbar").progressbar("option", "max", this._progressBarMax); - this._progressBarCurrentIncrement = 0; - $("#progressMsg").text('all resources registered.'); + this._progressBarCurrentValue = 0; } // all the resources have been downloaded: show the ok button @@ -635,23 +634,25 @@ export class GUI } // update progress bar: - else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE || signal.message === ServerManager.Event.RESOURCE_DOWNLOADED) + else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE + || signal.message === ServerManager.Event.RESOURCE_DOWNLOADED) { - if (typeof this._progressBarCurrentIncrement === 'undefined') + if (typeof this._progressBarCurrentValue === 'undefined') { - this._progressBarCurrentIncrement = 0; + this._progressBarCurrentValue = 0; } - ++this._progressBarCurrentIncrement; + ++this._progressBarCurrentValue; if (signal.message === ServerManager.Event.RESOURCE_DOWNLOADED) { - $("#progressMsg").text('downloaded ' + this._progressBarCurrentIncrement / 2 + ' / ' + this._progressBarMax / 2); + $("#progressMsg").text('downloaded ' + (this._progressBarCurrentValue / 2) + ' / ' + (this._progressBarMax / 2)); + } + else + { + $("#progressMsg").text('downloading ' + (this._progressBarCurrentValue / 2) + ' / ' + (this._progressBarMax / 2)); } // $("#progressMsg").text(signal.resource + ': downloaded.'); - // else - // $("#progressMsg").text(signal.resource + ': downloading...'); - - $("#progressbar").progressbar("option", "value", this._progressBarCurrentIncrement); + $("#progressbar").progressbar("option", "value", this._progressBarCurrentValue); } // unknown message: we just display it @@ -908,13 +909,13 @@ GUI.dialogMargin = [50, 50];
diff --git a/docs/core_Keyboard.js.html b/docs/core_Keyboard.js.html index 1792e0d..ec87b44 100644 --- a/docs/core_Keyboard.js.html +++ b/docs/core_Keyboard.js.html @@ -226,7 +226,7 @@ export class Keyboard extends PsychObject const keyEvent = this._circularBuffer[i]; if (keyEvent && keyEvent.status === Keyboard.KeyStatus.KEY_UP) { - // check that the key is in the keyList: + // if the keylist is empty of the key is in the keyList: if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey)) { // look for a corresponding, preceding keydown event: @@ -526,13 +526,13 @@ Keyboard.KeyStatus = {
diff --git a/docs/core_Logger.js.html b/docs/core_Logger.js.html index e398ec6..36a525c 100644 --- a/docs/core_Logger.js.html +++ b/docs/core_Logger.js.html @@ -340,9 +340,9 @@ export class Logger */ _customConsoleLayout() { - const detectedBrowser = this._psychoJS.browser; + const detectedBrowser = util.detectBrowser(); - const customLayout = new log4javascript.PatternLayout("%p %f{1} | %m"); + const customLayout = new log4javascript.PatternLayout("%p %d{HH:mm:ss.SSS} %f{1} | %m"); customLayout.setCustomField('location', function (layout, loggingReference) { // we throw a fake exception to retrieve the stack trace @@ -373,7 +373,7 @@ export class Logger const file = buf[buf.length - 3].split('/').pop(); const method = relevantEntry.split('@')[0]; - return method + ' ' + file + ' ' + line; + return method + ' ' + file + ':' + line; } else if (detectedBrowser === 'Safari') { @@ -391,7 +391,7 @@ export class Logger const line = buf.pop(); const file = buf.pop().split('/').pop(); - return method + ' ' + file + ' ' + line; + return method + ' ' + file + ':' + line; } else { @@ -474,13 +474,13 @@ Logger._ServerLevelValue = {
diff --git a/docs/core_MinimalStim.js.html b/docs/core_MinimalStim.js.html index 4466cfd..3ff925f 100644 --- a/docs/core_MinimalStim.js.html +++ b/docs/core_MinimalStim.js.html @@ -260,13 +260,13 @@ export class MinimalStim extends PsychObject
diff --git a/docs/core_Mouse.js.html b/docs/core_Mouse.js.html index 0e2bee3..0b7b6c7 100644 --- a/docs/core_Mouse.js.html +++ b/docs/core_Mouse.js.html @@ -397,13 +397,13 @@ export class Mouse extends PsychObject
diff --git a/docs/core_PsychoJS.js.html b/docs/core_PsychoJS.js.html index 188eed0..0f421dd 100644 --- a/docs/core_PsychoJS.js.html +++ b/docs/core_PsychoJS.js.html @@ -46,6 +46,7 @@ import {GUI} from './GUI'; import {MonotonicClock} from '../util/Clock'; import {Logger} from './Logger'; import * as util from '../util/Util'; +// import {Shelf} from "../data/Shelf"; /** @@ -58,6 +59,7 @@ import * as util from '../util/Util'; */ export class PsychoJS { + /** * Properties */ @@ -137,6 +139,11 @@ export class PsychoJS return this._browser; } + // get shelf() + // { + // return this._shelf; + // } + /** * @constructor @@ -182,6 +189,9 @@ export class PsychoJS // Window: this._window = undefined; + // // Shelf: + // this._shelf = new Shelf(this); + // redirection URLs: this._cancellationUrl = undefined; this._completionUrl = undefined; @@ -320,6 +330,17 @@ export class PsychoJS /** * Start the experiment. * + * <p>The resources are specified in the following fashion: + * <ul> + * <li>For an experiment running locally: the root directory for the specified resources is that of index.html + * unless they are prepended with a protocol, such as http:// or https://.</li> + * <li>For an experiment running on the server: if no resources are specified, all files in the resources directory + * of the experiment are downloaded, otherwise we only download the specified resources. All resources are assumed + * local to index.html unless they are prepended with a protocol.</li> + * <li>If resources is null: we do not download any resources.</li> + * </ul> + * </p> + * * @param {Object} options * @param {string} [options.configURL=config.json] - the URL of the configuration file * @param {string} [options.expName=UNKNOWN] - the name of the experiment @@ -327,8 +348,6 @@ export class PsychoJS * @param {Array.<{name: string, path: string}>} [resources=[]] - the list of resources * @async * @public - * - * @todo: close session on window or tab close */ async start({configURL = 'config.json', expName = 'UNKNOWN', expInfo = {}, resources = []} = {}) { @@ -414,11 +433,11 @@ export class PsychoJS // start the asynchronous download of resources: - this._serverManager.downloadResources(resources); + await this._serverManager.prepareResources(resources); // start the experiment: this.logger.info('[PsychoJS] Start Experiment.'); - this._scheduler.start(); + await this._scheduler.start(); } catch (error) { @@ -428,8 +447,12 @@ export class PsychoJS } + /** - * Synchronously download resources for the experiment. + * Block the experiment until the specified resources have been downloaded. + * + * <p>Note: only those resources that have not already been downloaded at that point are + * considered.</p> * * <ul> * <li>For an experiment running locally: the root directory for the specified resources is that of index.html @@ -439,14 +462,18 @@ export class PsychoJS * local to index.html unless they are prepended with a protocol.</li> * * @param {Array.<{name: string, path: string}>} [resources=[]] - the list of resources - * @async * @public */ - async downloadResources(resources = []) + waitForResources(resources = []) { + const response = { + origin: 'PsychoJS.waitForResources', + context: 'while waiting for resources to be downloaded' + }; + try { - await this.serverManager.downloadResources(resources); + return this.serverManager.waitForResources(resources); } catch (error) { @@ -456,6 +483,7 @@ export class PsychoJS } + /** * Make the attributes of the given object those of PsychoJS and those of * the top level variable (e.g. window) as well. @@ -631,6 +659,14 @@ export class PsychoJS { throw 'missing URL in pavlovia block in configuration'; } + if (!('gitlab' in this._config)) + { + throw 'missing gitlab block in configuration'; + } + if (!('projectId' in this._config.gitlab)) + { + throw 'missing projectId in gitlab block in configuration'; + } this._config.environment = ExperimentHandler.Environment.SERVER; @@ -713,23 +749,21 @@ export class PsychoJS */ _captureErrors() { - this.logger.debug('capturing all errors using window.onerror'); + this.logger.debug('capturing all errors and showing them in a pop up window'); const self = this; window.onerror = function (message, source, lineno, colno, error) { console.error(error); - self._gui.dialog({"error": error}); + self._gui.dialog({error}); return true; }; - - /* NOT UNIVERSALLY SUPPORTED YET - window.addEventListener('unhandledrejection', event => { - console.error(error); - self._gui.dialog({"error" : error}); + window.onunhandledrejection = function (error) + { + console.error(error.reason); + self._gui.dialog({error: error.reason}); return true; - });*/ - + }; } @@ -765,9 +799,10 @@ PsychoJS.Status = { CONFIGURED: Symbol.for('CONFIGURED'), NOT_STARTED: Symbol.for('NOT_STARTED'), STARTED: Symbol.for('STARTED'), + PAUSED: Symbol.for('PAUSED'), FINISHED: Symbol.for('FINISHED'), - - STOPPED: Symbol.for('FINISHED') //Symbol.for('STOPPED') + STOPPED: Symbol.for('FINISHED'), //Symbol.for('STOPPED') + ERROR: Symbol.for('ERROR') }; @@ -780,13 +815,13 @@ PsychoJS.Status = {
diff --git a/docs/core_ServerManager.js.html b/docs/core_ServerManager.js.html index b3fcd2c..94f3e94 100644 --- a/docs/core_ServerManager.js.html +++ b/docs/core_ServerManager.js.html @@ -27,7 +27,7 @@
/**
- * Manager responsible for the communication between the experiment running in the participant's browser and the remote PsychoJS manager running on the remote https://pavlovia.org server.
+ * Manager responsible for the communication between the experiment running in the participant's browser and the pavlovia.org server.
  *
  * @author Alain Pitiot
  * @version 2021.1.4
@@ -46,9 +46,8 @@ import {MonotonicClock} from "../util/Clock";
 
 
 /**
- * <p>This manager handles all communications between the experiment running in the participant's browser and the remote PsychoJS manager running on the [pavlovia.org]{@link http://pavlovia.org} server, <em>in an asynchronous manner</em>.</p>
- * <p>It is responsible for reading the configuration file of an experiment, for opening and closing a session, for listing and downloading resources, and for uploading results and log.</p>
- * <p>Note: The Server Manager uses [Promises]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise} to deal with asynchronicity, is mostly called by {@link PsychoJS}, and is not exposed to the experiment code.</p>
+ * <p>This manager handles all communications between the experiment running in the participant's browser and the [pavlovia.org]{@link http://pavlovia.org} server, <em>in an asynchronous manner</em>.</p>
+ * <p>It is responsible for reading the configuration file of an experiment, for opening and closing a session, for listing and downloading resources, and for uploading results, logs, and audio recordings.</p>
  *
  * @name module:core.ServerManager
  * @class
@@ -59,6 +58,16 @@ import {MonotonicClock} from "../util/Clock";
  */
 export class ServerManager extends PsychObject
 {
+	/**
+	 * Used to indicate to the ServerManager that all resources must be registered (and
+	 * subsequently downloaded)
+	 *
+	 * @type {symbol}
+	 * @readonly
+	 * @public
+	 */
+	static ALL_RESOURCES = Symbol.for('ALL_RESOURCES');
+
 
 	constructor({
 								psychoJS,
@@ -70,9 +79,8 @@ export class ServerManager extends PsychObject
 		// session:
 		this._session = {};
 
-		// resources is a map of {name: string, path: string} -> data: any
+		// resources is a map of <name: string, { path: string, status: ResourceStatus, data: any }>
 		this._resources = new Map();
-		this._nbResources = -1;
 
 		this._addAttribute('autoLog', autoLog);
 		this._addAttribute('status', ServerManager.Status.READY);
@@ -300,25 +308,65 @@ export class ServerManager extends PsychObject
 	 * @name module:core.ServerManager#getResource
 	 * @function
 	 * @public
-	 * @param {string} name of the requested resource
-	 * @return {Object} value of the resource
+	 * @param {string} name - name of the requested resource
+	 * @param {boolean} [errorIfNotDownloaded = false] whether or not to throw an exception if the
+	 * resource status is not DOWNLOADED
+	 * @return {Object} value of the resource, or undefined if the resource has been registered
+	 * but not downloaded yet.
 	 * @throws {Object.<string, *>} exception if no resource with that name has previously been registered
 	 */
-	getResource(name)
+	getResource(name, errorIfNotDownloaded = false)
 	{
 		const response = {
 			origin: 'ServerManager.getResource',
 			context: 'when getting the value of resource: ' + name
 		};
 
-		const path_data = this._resources.get(name);
-		if (typeof path_data === 'undefined')
-		// throw { ...response, error: 'unknown resource' };
+		const pathStatusData = this._resources.get(name);
+
+		if (typeof pathStatusData === 'undefined')
 		{
+			// throw { ...response, error: 'unknown resource' };
 			throw Object.assign(response, {error: 'unknown resource'});
 		}
 
-		return path_data.data;
+		if (errorIfNotDownloaded && pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED)
+		{
+			throw Object.assign(response, {
+				error: name + ' is not available for use (yet), its current status is: ' +
+					util.toString(pathStatusData.status)
+			});
+		}
+
+		return pathStatusData.data;
+	}
+
+
+	/**
+	 * Get the status of a resource.
+	 *
+	 * @name module:core.ServerManager#getResourceStatus
+	 * @function
+	 * @public
+	 * @param {string} name of the requested resource
+	 * @return {core.ServerManager.ResourceStatus} status of the resource
+	 * @throws {Object.<string, *>} exception if no resource with that name has previously been registered
+	 */
+	getResourceStatus(name)
+	{
+		const response = {
+			origin: 'ServerManager.getResourceStatus',
+			context: 'when getting the status of resource: ' + name
+		};
+
+		const pathStatusData = this._resources.get(name);
+		if (typeof pathStatusData === 'undefined')
+		{
+			// throw { ...response, error: 'unknown resource' };
+			throw Object.assign(response, {error: 'unknown resource'});
+		}
+
+		return pathStatusData.status;
 	}
 
 
@@ -373,7 +421,8 @@ export class ServerManager extends PsychObject
 
 
 	/**
-	 * Asynchronously download resources for the experiment and register them with the server manager.
+	 * Prepare resources for the experiment: register them with the server manager and possibly
+	 * start downloading them right away.
 	 *
 	 * <ul>
 	 *   <li>For an experiment running locally: the root directory for the specified resources is that of index.html
@@ -381,85 +430,218 @@ export class ServerManager extends PsychObject
 	 *   <li>For an experiment running on the server: if no resources are specified, all files in the resources directory
 	 *   of the experiment are downloaded, otherwise we only download the specified resources. All resources are assumed
 	 *   local to index.html unless they are prepended with a protocol.</li>
+	 *   <li>If resources is null, then we do not download any resources</li>
 	 * </ul>
 	 *
-	 * @name module:core.ServerManager#downloadResources
+	 * @name module:core.ServerManager#prepareResources
+	 * @param {Array.<{name: string, path: string, download: boolean} | Symbol>} [resources=[]] - the list of resources
+	 * @function
+	 * @public
+	 */
+	async prepareResources(resources = [])
+	{
+		const response = {
+			origin: 'ServerManager.prepareResources',
+			context: 'when preparing resources for experiment: ' + this._psychoJS.config.experiment.name
+		};
+
+		this._psychoJS.logger.debug('preparing resources for experiment: ' + this._psychoJS.config.experiment.name);
+
+		try
+		{
+			const resourcesToDownload = new Set();
+
+			// register the resources:
+			if (resources !== null)
+			{
+				if (!Array.isArray(resources))
+				{
+					throw "resources should be an array of objects";
+				}
+
+				// whether all resources have been requested:
+
+				const allResources = (resources.length === 1 && resources[0] === ServerManager.ALL_RESOURCES);
+
+				// if the experiment is hosted on the pavlovia.org server and
+				// resources is [ServerManager.ALL_RESOURCES], then we register all the resources
+				// in the "resources" sub-directory
+				if (this._psychoJS.config.environment === ExperimentHandler.Environment.SERVER
+					&& allResources)
+				{
+					// list the resources from the resources directory of the experiment on the server:
+					const serverResponse = await this._listResources();
+
+					// register and mark for download those resources that have not been
+					// registered already:
+					for (const name of serverResponse.resources)
+					{
+						if (!this._resources.has(name))
+						{
+							const path = serverResponse.resourceDirectory + '/' + name;
+							this._resources.set(name, {
+								status: ServerManager.ResourceStatus.REGISTERED,
+								path,
+								data: undefined
+							});
+							this._psychoJS.logger.debug('registered resource:', name, path);
+							resourcesToDownload.add(name);
+						}
+					}
+				}
+
+				// if the experiment is hosted locally (localhost) or if specific resources were given
+				// then we register those specific resources, if they have not been registered already
+				else
+				{
+					// we cannot ask for all resources to be registered locally, since we cannot list
+					// them:
+					if (this._psychoJS.config.environment === ExperimentHandler.Environment.LOCAL
+						&& allResources)
+					{
+						throw "resources must be manually specified when the experiment is running locally: ALL_RESOURCES cannot be used";
+					}
+
+					for (let {name, path, download} of resources)
+					{
+						if (!this._resources.has(name))
+						{
+							// to deal with potential CORS issues, we use the pavlovia.org proxy for resources
+							// not hosted on pavlovia.org:
+							if ((path.toLowerCase().indexOf('www.') === 0 ||
+								path.toLowerCase().indexOf('http:') === 0 ||
+								path.toLowerCase().indexOf('https:') === 0) &&
+								(path.indexOf('pavlovia.org') === -1))
+							{
+								path = 'https://pavlovia.org/api/v2/proxy/' + path;
+							}
+
+							this._resources.set(name, {
+								status: ServerManager.ResourceStatus.REGISTERED,
+								path,
+								data: undefined
+							});
+							this._psychoJS.logger.debug('registered resource:', name, path);
+
+							// download resources by default:
+							if (typeof download === 'undefined' || download)
+							{
+								resourcesToDownload.add(name);
+							}
+						}
+					}
+				}
+			}
+
+			// download those registered resources for which download = true:
+			/*await*/ this._downloadResources(resourcesToDownload);
+		}
+		catch (error)
+		{
+			console.log('error', error);
+			throw Object.assign(response, {error});
+			// throw { ...response, error: error };
+		}
+	}
+
+
+	/**
+	 * Block the experiment until the specified resources have been downloaded.
+	 *
+	 * @name module:core.ServerManager#waitForResources
 	 * @param {Array.<{name: string, path: string}>} [resources=[]] - the list of resources
 	 * @function
 	 * @public
 	 */
-	downloadResources(resources = [])
+	waitForResources(resources = [])
 	{
-		const response = {
-			origin: 'ServerManager.downloadResources',
-			context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name
+		// prepare a PsychoJS component:
+		this._waitForDownloadComponent = {
+			status: PsychoJS.Status.NOT_STARTED,
+			clock: new Clock(),
+			resources: new Set()
 		};
 
-		this._psychoJS.logger.debug('downloading resources for experiment: ' + this._psychoJS.config.experiment.name);
-
-		// we use an anonymous async function here since downloadResources is non-blocking (asynchronous)
-		// but we want to run the asynchronous _listResources and _downloadResources in sequence
 		const self = this;
-		const newResources = new Map();
-		let download = async () =>
+		return () =>
 		{
-			try
+			const t = self._waitForDownloadComponent.clock.getTime();
+
+			// start the component:
+			if (t >= 0.0 && self._waitForDownloadComponent.status === PsychoJS.Status.NOT_STARTED)
 			{
-				if (self._psychoJS.config.environment === ExperimentHandler.Environment.SERVER)
+				self._waitForDownloadComponent.tStart = t;
+				self._waitForDownloadComponent.status = PsychoJS.Status.STARTED;
+
+				// if resources is an empty array, we consider all registered resources:
+				if (resources.length === 0)
 				{
-					// no resources specified, we register them all:
-					if (resources.length === 0)
+					for (const [name, {status, path, data}] of this._resources)
 					{
-						// list the resources from the resources directory of the experiment on the server:
-						const serverResponse = await self._listResources();
-						for (const name of serverResponse.resources)
-						{
-							self._resources.set(name, {path: serverResponse.resourceDirectory + '/' + name});
-						}
-					}
-					else
-					{
-						// only registered the specified resources:
-						for (const {name, path} of resources)
-						{
-							self._resources.set(name, {path});
-							newResources.set(name, {path});
-						}
-					}
-				}
-				else
-				{
-					// register the specified resources:
-					for (const {name, path} of resources)
-					{
-						self._resources.set(name, {path});
-						newResources.set(name, {path});
+						resources.append({ name, path });
 					}
 				}
 
-				self._nbResources = self._resources.size;
-				for (const name of self._resources.keys())
+				// only download those resources not already downloaded or downloading:
+				const resourcesToDownload = new Set();
+				for (let {name, path} of resources)
 				{
-					this._psychoJS.logger.debug('resource:', name, self._resources.get(name).path);
+					// to deal with potential CORS issues, we use the pavlovia.org proxy for resources
+					// not hosted on pavlovia.org:
+					if ( (path.toLowerCase().indexOf('www.') === 0 ||
+						path.toLowerCase().indexOf('http:') === 0 ||
+						path.toLowerCase().indexOf('https:') === 0) &&
+						(path.indexOf('pavlovia.org') === -1) )
+					{
+						path = 'https://devlovia.org/api/v2/proxy/' + path;
+					}
+
+					const pathStatusData = this._resources.get(name);
+
+					// the resource has not been registered yet:
+					if (typeof pathStatusData === 'undefined')
+					{
+						self._resources.set(name, {
+							status: ServerManager.ResourceStatus.REGISTERED,
+							path,
+							data: undefined
+						});
+						self._waitForDownloadComponent.resources.add(name);
+						resourcesToDownload.add(name);
+						self._psychoJS.logger.debug('registered resource:', name, path);
+					}
+
+					// the resource has been registered but is not downloaded yet:
+					else if (typeof pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED)
+						// else if (typeof pathStatusData.data === 'undefined')
+					{
+						self._waitForDownloadComponent.resources.add(name);
+					}
+
 				}
 
-				self.emit(ServerManager.Event.RESOURCE, {
-					message: ServerManager.Event.RESOURCES_REGISTERED,
-					count: self._nbResources
-				});
-
-				// download the registered resources:
-				await self._downloadRegisteredResources(newResources);
+				// start the download:
+				self._downloadResources(resourcesToDownload);
 			}
-			catch (error)
+
+			// check whether all resources have been downloaded:
+			for (const name of self._waitForDownloadComponent.resources)
 			{
-				console.log('error', error);
-				// throw { ...response, error: error };
-				throw Object.assign(response, {error});
+				const pathStatusData = this._resources.get(name);
+
+				// the resource has not been downloaded yet: loop this component
+				if (typeof pathStatusData.status !== ServerManager.ResourceStatus.DOWNLOADED)
+					// if (typeof pathStatusData.data === 'undefined')
+				{
+					return Scheduler.Event.FLIP_REPEAT;
+				}
 			}
+
+			// all resources have been downloaded: move to the next component:
+			self._waitForDownloadComponent.status = PsychoJS.Status.FINISHED;
+			return Scheduler.Event.NEXT;
 		};
 
-		download();
 	}
 
 
@@ -470,7 +652,7 @@ export class ServerManager extends PsychObject
 	 * @property {Object.<string, *>} [error] an error message if we could not upload the data
 	 */
 	/**
-	 * Asynchronously upload experiment data to the remote PsychoJS manager.
+	 * Asynchronously upload experiment data to the pavlovia server.
 	 *
 	 * @name module:core.ServerManager#uploadData
 	 * @function
@@ -535,8 +717,9 @@ export class ServerManager extends PsychObject
 	}
 
 
+
 	/**
-	 * Asynchronously upload experiment logs to the remote PsychoJS manager.
+	 * Asynchronously upload experiment logs to the pavlovia server.
 	 *
 	 * @name module:core.ServerManager#uploadLog
 	 * @function
@@ -595,6 +778,84 @@ export class ServerManager extends PsychObject
 	}
 
 
+
+	/**
+	 * Asynchronously upload audio data to the pavlovia server.
+	 *
+	 * @name module:core.ServerManager#uploadAudio
+	 * @function
+	 * @public
+	 * @param {Blob} audioBlob - the audio blob to be uploaded
+	 * @param {string} tag - additional tag
+	 * @returns {Promise<ServerManager.UploadDataPromise>} the response
+	 */
+	async uploadAudio(audioBlob, tag)
+	{
+		const response = {
+			origin: 'ServerManager.uploadAudio',
+			context: 'when uploading audio data for experiment: ' + this._psychoJS.config.experiment.fullpath
+		};
+
+		try
+		{
+			if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER ||
+				this._psychoJS.config.experiment.status !== 'RUNNING' ||
+				this._psychoJS._serverMsg.has('__pilotToken'))
+			{
+				throw 'audio recordings can only be uploaded to the server for experiments running on the server';
+			}
+
+			this._psychoJS.logger.debug('uploading audio data for experiment: ' + this._psychoJS.config.experiment.fullpath);
+			this.setStatus(ServerManager.Status.BUSY);
+
+			// prepare the request:
+			const info = this.psychoJS.experiment.extraInfo;
+			const participant = ((typeof info.participant === 'string' && info.participant.length > 0) ? info.participant : 'PARTICIPANT');
+			const experimentName = (typeof info.expName !== 'undefined') ? info.expName : this.psychoJS.config.experiment.name;
+			const datetime = ((typeof info.date !== 'undefined') ? info.date : MonotonicClock.getDateStr());
+			const filename = participant + '_' + experimentName + '_' + datetime + '_' + tag;
+
+			const formData = new FormData();
+			formData.append('audio', audioBlob, filename);
+
+			const url = this._psychoJS.config.pavlovia.URL +
+				'/api/v2/experiments/' + this._psychoJS.config.gitlab.projectId +
+				'/sessions/' + this._psychoJS.config.session.token +
+				'/audio';
+
+			// query the pavlovia server:
+			const response = await fetch(url, {
+				method: 'POST',
+				mode: 'cors', // no-cors, *cors, same-origin
+				cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
+				credentials: 'same-origin', // include, *same-origin, omit
+				redirect: 'follow', // manual, *follow, error
+				referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
+				body: formData
+			});
+			const jsonResponse = await response.json();
+
+			// deal with server errors:
+			if (!response.ok)
+			{
+				throw jsonResponse;
+			}
+
+			this.setStatus(ServerManager.Status.READY);
+			return jsonResponse;
+		}
+		catch (error)
+		{
+			this.setStatus(ServerManager.Status.ERROR);
+			console.error(error);
+
+			throw {...response, error};
+		}
+
+	}
+
+
+
 	/**
 	 * List the resources available to the experiment.
 
@@ -664,62 +925,76 @@ export class ServerManager extends PsychObject
 	}
 
 
+
 	/**
-	 * Download the resources previously registered.
+	 * Download the specified resources.
 	 *
 	 * <p>Note: we use the [preloadjs library]{@link https://www.createjs.com/preloadjs}.</p>
 	 *
-	 * @name module:core.ServerManager#_downloadRegisteredResources
+	 * @name module:core.ServerManager#_downloadResources
 	 * @function
-	 * @private
+	 * @protected
+	 * @param {Set} resources - a set of names of previously registered resources
 	 */
-	_downloadRegisteredResources(resources = new Map())
+	_downloadResources(resources)
 	{
 		const response = {
 			origin: 'ServerManager._downloadResources',
-			context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name
+			context: 'when downloading resources for experiment: ' + this._psychoJS.config.experiment.name
 		};
 
-		this._psychoJS.logger.debug('downloading the registered resources for experiment: ' + this._psychoJS.config.experiment.name);
+		this._psychoJS.logger.debug('downloading resources for experiment: ' + this._psychoJS.config.experiment.name);
 
 		this.setStatus(ServerManager.Status.BUSY);
+		this.emit(ServerManager.Event.RESOURCE, {
+			message: ServerManager.Event.DOWNLOADING_RESOURCES,
+			count: resources.size
+		});
+
 		this._nbLoadedResources = 0;
 
 
 		// (*) set-up preload.js:
-		this._resourceQueue = new createjs.LoadQueue(true); //, this._psychoJS.config.experiment.resourceDirectory);
+		this._resourceQueue = new createjs.LoadQueue(true, '', true);
 
 		const self = this;
 
-		const filesToDownload = resources.size ? resources : this._resources;
-
+		// the loading of a specific resource has started:
 		this._resourceQueue.addEventListener("filestart", event =>
 		{
+			const pathStatusData = self._resources.get(event.item.id);
+			pathStatusData.status =  ServerManager.ResourceStatus.DOWNLOADING;
+
 			self.emit(ServerManager.Event.RESOURCE, {
 				message: ServerManager.Event.DOWNLOADING_RESOURCE,
 				resource: event.item.id
 			});
 		});
 
+		// the loading of a specific resource has completed:
 		this._resourceQueue.addEventListener("fileload", event =>
 		{
-			++self._nbLoadedResources;
-			let path_data = self._resources.get(event.item.id);
-			path_data.data = event.result;
+			const pathStatusData = self._resources.get(event.item.id);
+			pathStatusData.data = event.result;
+			pathStatusData.status =  ServerManager.ResourceStatus.DOWNLOADED;
+
+			++ self._nbLoadedResources;
 			self.emit(ServerManager.Event.RESOURCE, {
 				message: ServerManager.Event.RESOURCE_DOWNLOADED,
 				resource: event.item.id
 			});
 		});
 
-		// loading completed:
+		// the loading of all given resources completed:
 		this._resourceQueue.addEventListener("complete", event =>
 		{
 			self._resourceQueue.close();
-			if (self._nbLoadedResources === filesToDownload.size)
+			if (self._nbLoadedResources === resources.size)
 			{
 				self.setStatus(ServerManager.Status.READY);
-				self.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
+				self.emit(ServerManager.Event.RESOURCE, {
+					message: ServerManager.Event.DOWNLOAD_COMPLETED
+				});
 			}
 		});
 
@@ -727,16 +1002,43 @@ export class ServerManager extends PsychObject
 		this._resourceQueue.addEventListener("error", event =>
 		{
 			self.setStatus(ServerManager.Status.ERROR);
-			const resourceId = (typeof event.data !== 'undefined') ? event.data.id : 'UNKNOWN RESOURCE';
-			// throw { ...response, error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' };
-			throw Object.assign(response, {error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')'});
+			if (typeof event.item !== 'undefined')
+			{
+				const pathStatusData = self._resources.get(event.item.id);
+				pathStatusData.status =  ServerManager.ResourceStatus.ERROR;
+				throw Object.assign(response, {
+					error: 'unable to download resource: ' + event.item.id + ' (' + event.title + ')'
+				});
+			}
+			else
+			{
+				console.error(event);
+
+				if (event.title === 'FILE_LOAD_ERROR' && typeof event.data !== 'undefined')
+				{
+					const id = event.data.id;
+					const title = event.data.src;
+
+					throw Object.assign(response, {
+						error: 'unable to download resource: ' + id + ' (' + title + ')'
+					});
+				}
+
+				else
+				{
+					throw Object.assign(response, {
+						error: 'unspecified download error'
+					});
+				}
+
+			}
 		});
 
 
 		// (*) dispatch resources to preload.js or howler.js based on extension:
 		let manifest = [];
-		let soundResources = [];
-		for (const [name, path_data] of filesToDownload)
+		const soundResources = new Set();
+		for (const name of resources)
 		{
 			const nameParts = name.toLowerCase().split('.');
 			const extension = (nameParts.length > 1) ? nameParts.pop() : undefined;
@@ -747,10 +1049,25 @@ export class ServerManager extends PsychObject
 				this.psychoJS.logger.warn(`"${name}" does not appear to have an extension, which may negatively impact its loading. We highly recommend you add an extension.`);
 			}
 
+			const pathStatusData = this._resources.get(name);
+			if (typeof pathStatusData === 'undefined')
+			{
+				throw Object.assign(response, {error: name + ' has not been previously registered'});
+			}
+			if (pathStatusData.status !== ServerManager.ResourceStatus.REGISTERED)
+			{
+				throw Object.assign(response, {error: name + ' is already downloaded or is currently already downloading'});
+			}
+
 			// preload.js with forced binary for xls and xlsx:
 			if (['csv', 'odp', 'xls', 'xlsx'].indexOf(extension) > -1)
 			{
-				manifest.push({id: name, src: path_data.path, type: createjs.Types.BINARY});
+				manifest.push(new createjs.LoadItem().set({
+					id: name,
+					src: pathStatusData.path,
+					type: createjs.Types.BINARY,
+					crossOrigin: 'Anonymous'
+				}));
 			}/* ascii .csv are adequately handled in binary format
 			// forced text for .csv:
 			else if (['csv'].indexOf(resourceExtension) > -1)
@@ -760,7 +1077,7 @@ export class ServerManager extends PsychObject
 			// sound files are loaded through howler.js:
 			else if (['mp3', 'mpeg', 'opus', 'ogg', 'oga', 'wav', 'aac', 'caf', 'm4a', 'weba', 'dolby', 'flac'].indexOf(extension) > -1)
 			{
-				soundResources.push(name);
+				soundResources.add(name);
 
 				if (extension === 'wav')
 				{
@@ -771,7 +1088,11 @@ export class ServerManager extends PsychObject
 			// preload.js for the other extensions (download type decided by preload.js):
 			else
 			{
-				manifest.push({id: name, src: path_data.path});
+				manifest.push(new createjs.LoadItem().set({
+					id: name,
+					src: pathStatusData.path,
+					crossOrigin: 'Anonymous'
+				}));
 			}
 		}
 
@@ -783,10 +1104,11 @@ export class ServerManager extends PsychObject
 		}
 		else
 		{
-			if (this._nbLoadedResources === filesToDownload.size)
+			if (this._nbLoadedResources === resources.size)
 			{
 				this.setStatus(ServerManager.Status.READY);
-				this.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
+				this.emit(ServerManager.Event.RESOURCE, {
+					message: ServerManager.Event.DOWNLOAD_COMPLETED});
 			}
 		}
 
@@ -794,33 +1116,37 @@ export class ServerManager extends PsychObject
 		// (*) prepare and start loading sound resources:
 		for (const name of soundResources)
 		{
-			self.emit(ServerManager.Event.RESOURCE, {
+			const pathStatusData = this._resources.get(name);
+			pathStatusData.status =  ServerManager.ResourceStatus.DOWNLOADING;
+			this.emit(ServerManager.Event.RESOURCE, {
 				message: ServerManager.Event.DOWNLOADING_RESOURCE,
 				resource: name
 			});
-			const path_data = self._resources.get(name);
 			const howl = new Howl({
-				src: path_data.path,
+				src: pathStatusData.path,
 				preload: false,
 				autoplay: false
 			});
 
 			howl.on('load', (event) =>
 			{
-				++self._nbLoadedResources;
-				path_data.data = howl;
-				// self._resources.set(resource.name, howl);
+				++ self._nbLoadedResources;
+				pathStatusData.data = howl;
+
+				pathStatusData.status =  ServerManager.ResourceStatus.DOWNLOADED;
 				self.emit(ServerManager.Event.RESOURCE, {
 					message: ServerManager.Event.RESOURCE_DOWNLOADED,
 					resource: name
 				});
 
-				if (self._nbLoadedResources === filesToDownload.size)
+				if (self._nbLoadedResources === resources.size)
 				{
 					self.setStatus(ServerManager.Status.READY);
-					self.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
+					self.emit(ServerManager.Event.RESOURCE, {
+						message: ServerManager.Event.DOWNLOAD_COMPLETED});
 				}
 			});
+
 			howl.on('loaderror', (id, error) =>
 			{
 				// throw { ...response, error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' };
@@ -829,6 +1155,7 @@ export class ServerManager extends PsychObject
 
 			howl.load();
 		}
+
 	}
 
 }
@@ -849,22 +1176,26 @@ ServerManager.Event = {
 	 * Event type: resource event
 	 */
 	RESOURCE: Symbol.for('RESOURCE'),
+
 	/**
-	 * Event: resources all registered
+	 * Event: resources have started to download
 	 */
-	RESOURCES_REGISTERED: Symbol.for('RESOURCES_REGISTERED'),
+	DOWNLOADING_RESOURCES: Symbol.for('DOWNLOADING_RESOURCES'),
+
 	/**
-	 * Event: resource download has started
+	 * Event: a specific resource download has started
 	 */
 	DOWNLOADING_RESOURCE: Symbol.for('DOWNLOADING_RESOURCE'),
+
 	/**
-	 * Event: resource has been downloaded
+	 * Event: a specific resource has been downloaded
 	 */
 	RESOURCE_DOWNLOADED: Symbol.for('RESOURCE_DOWNLOADED'),
+
 	/**
-	 * Event: resources all downloaded
+	 * Event: resources have all downloaded
 	 */
-	DOWNLOAD_COMPLETED: Symbol.for('DOWNLOAD_COMPLETED'),
+	DOWNLOADS_COMPLETED: Symbol.for('DOWNLOAD_COMPLETED'),
 
 	/**
 	 * Event type: status event
@@ -897,6 +1228,37 @@ ServerManager.Status = {
 	 */
 	ERROR: Symbol.for('ERROR')
 };
+
+
+/**
+ * Resource status
+ *
+ * @name module:core.ServerManager#ResourceStatus
+ * @enum {Symbol}
+ * @readonly
+ * @public
+ */
+ServerManager.ResourceStatus = {
+	/**
+	 * The resource has been registered.
+	 */
+	REGISTERED: Symbol.for('REGISTERED'),
+
+	/**
+	 * The resource is currently downloading.
+	 */
+	DOWNLOADING: Symbol.for('DOWNLOADING'),
+
+	/**
+	 * The resource has been downloaded.
+	 */
+	DOWNLOADED: Symbol.for('DOWNLOADED'),
+
+	/**
+	 * There was an error during downloading, or the resource is in an unknown state.
+	 */
+	ERROR: Symbol.for('ERROR'),
+};
 
@@ -907,13 +1269,13 @@ ServerManager.Status = {
diff --git a/docs/core_Window.js.html b/docs/core_Window.js.html index f79e21e..aeff689 100644 --- a/docs/core_Window.js.html +++ b/docs/core_Window.js.html @@ -35,9 +35,9 @@ * @license Distributed under the terms of the MIT License */ -import {Color} from '../util'; -import {PsychObject} from '../util'; -import {MonotonicClock} from '../util'; +import {Color} from '../util/Color'; +import {PsychObject} from '../util/PsychObject'; +import {MonotonicClock} from '../util/Clock'; import {Logger} from "./Logger"; /** @@ -551,13 +551,13 @@ export class Window extends PsychObject
diff --git a/docs/core_WindowMixin.js.html b/docs/core_WindowMixin.js.html index 6c685b2..a7935d5 100644 --- a/docs/core_WindowMixin.js.html +++ b/docs/core_WindowMixin.js.html @@ -229,13 +229,13 @@ export let WindowMixin = (superclass) => class extends superclass
diff --git a/docs/data_ExperimentHandler.js.html b/docs/data_ExperimentHandler.js.html index b977d22..ff125da 100644 --- a/docs/data_ExperimentHandler.js.html +++ b/docs/data_ExperimentHandler.js.html @@ -487,13 +487,13 @@ ExperimentHandler.Environment = {
diff --git a/docs/data_TrialHandler.js.html b/docs/data_TrialHandler.js.html index 3a65e2c..00431b7 100644 --- a/docs/data_TrialHandler.js.html +++ b/docs/data_TrialHandler.js.html @@ -239,7 +239,6 @@ export class TrialHandler extends PsychObject * @property {number} ran - whether or not the trial ran * @property {number} finished - whether or not the trials finished */ - /** * Get a snapshot of the current internal state of the trial handler (e.g. current trial number, * number of trial remaining). @@ -461,7 +460,7 @@ export class TrialHandler extends PsychObject if (['csv', 'odp', 'xls', 'xlsx'].indexOf(resourceExtension) > -1) { // (*) read conditions from resource: - const resourceValue = serverManager.getResource(resourceName); + const resourceValue = serverManager.getResource(resourceName, true); // Conditionally use a `TextDecoder` to reprocess .csv type input, // which is then read in as a string @@ -727,13 +726,13 @@ TrialHandler.Method = {
diff --git a/docs/index.html b/docs/index.html index c27d3d8..815d98e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -17,68 +17,30 @@
-
-

PsychoJS

-

PsychoJS is a JavaScript library that makes it possible to run neuroscience, psychology, and psychophysics experiments in a browser. It is the online counterpart of the PsychoPy Python library. -It is also a git submodule: psychopy/psychojs

-

Motivation

-

Many studies in behavioural sciences (e.g. psychology, neuroscience, linguistics or mental health) use computers to present stimuli and record responses in a precise manner. These studies are still typically conducted on small numbers of people in laboratory environments equipped with dedicated hardware.

-

With high-speed broadband, improved web technologies and smart devices everywhere, studies can now go online without sacrificing too much temporal precision. This is a “game changer”. Data can be collected on larger, more varied, international populations. We can study people in environments they do not find intimidating. Experiments can be run multiple times per day, without data collection becoming impractical.

-

The idea behind PsychoJS is to make PsychoPy experiments available online, from a web page, so participants can run them on any device equipped with a web browser such as desktops, laptops, or tablets. In some circumstance, they can even use their phone!

-

Getting Started

-

Running PsychoPy experiments online requires the generation of an index.html file and of a javascript file that contains the code describing the experiment. Those files need to be hosted on a web server to which participants will point their browser in order to run the experiment. The server will also need to host the PsychoJS library.

-

PsychoPy Builder

-

Starting with PsychoPy version 3.0, PsychoPy Builder can automatically generate the javascript and html files. Many of the existing Builder experiments should "just work", subject to the Components being currently supported by PsychoJS (see below).

-

JavaScript Code

-

We built the PsychoJS library to make the JavaScript experiment files look and behave in very much the same way as to the Builder-generated Python files. PsychoJS offers classes such as Window and ImageStim, with very similar attributes to their Python equivalents. Experiment designers familiar with the PsychoPy library should feel at home with PsychoJS, and can expect the same level of control they have with PsychoPy, from the structure of the trials/loops all the way down to frame-by-frame updates.

-

There are however notable differences between the PsychoJS and PsychoPy libraries, most of which have to do with the way a web browser interprets and runs JavaScript, deals with resources (such as images, sound or videos), or render stimuli. To manage those web-specific aspect, PsychoJS introduces the concept of Scheduler. As the name indicate, Scheduler's offer a way to organise various PsychoJS along a timeline, such as downloading resources, running a loop, checking for keyboard input, saving experiment results, etc. As an illustration, a Flow in PsychoPy can be conceptualised as a Schedule, with various tasks on it. Some of those tasks, such as trial loops, can also schedule further events (i.e. the individual trials to be run).

-

Under the hood PsychoJS relies on PixiJs to present stimuli and collect responses. PixiJs is a multi-platform, accelerated, 2-D renderer, that runs in most modern browsers. It uses WebGL wherever possible and silently falls back to HTML5 canvas where not. WebGL directly addresses the graphic card, thereby considerably improving the rendering performance.

-

Hosting Experiments

-

A convenient way to make experiment available to participants is to host them on pavlovia.org, an open-science server under active development. PsychoPy Builder offers the possibility of uploading the experiment directly to pavlovia.org.

-

Which PsychoPy Components are supported by PsychoJS?

-

PsychoJS currently supports the following Components:

-

Stimuli:

-
    -
  • Form
  • -
  • Image
  • -
  • Rect
  • -
  • Shape (Polygon)
  • -
  • Slider
  • -
  • Sound (tones and tracks)
  • -
  • Text
  • -
  • TextBox
  • -
  • Video
  • -
-

Events:

-
    -
  • Keyboard
  • -
  • Mouse
  • -
-

We are constantly adding new Components and are regularly updating this list.

-

API

-

The full documentation of the PsychoJS API is here.

-

Maintainers

-

Alain Pitiot - @apitiot

-

Contributors

-

The PsychoJS library was initially written by Ilixa with support from the Wellcome Trust. -It is now a collaborative effort, supported by the Chan Zuckerberg Initiative (2020-2021) and Open Science Tools (2020-):

- -

The PsychoPy Builder's javascript code generator is built and maintained by the creators of PsychoPy at the University of Nottingham, with support from the Wellcome Trust (2017-2019), from the Chan Zuckerberg Initiative (2020-2021), and from Open Science Tools (2020-):

- -

License

-

This project is licensed under the MIT License - see the LICENSE.md file for details.

-
+

Home

+ + + + + + + + +

+ + + + + + + + + + + + + + @@ -88,16 +50,16 @@ It is now a collaborative effort, supported by the Home

Modules

Classes

Interfaces

Mixins

+

Home

Modules

Classes

Interfaces

Mixins


- + \ No newline at end of file diff --git a/docs/module-core.BuilderKeyResponse.html b/docs/module-core.BuilderKeyResponse.html index 4496e1a..1eafc6f 100644 --- a/docs/module-core.BuilderKeyResponse.html +++ b/docs/module-core.BuilderKeyResponse.html @@ -254,13 +254,13 @@

diff --git a/docs/module-core.EventManager.html b/docs/module-core.EventManager.html index 5c999ca..55e4227 100644 --- a/docs/module-core.EventManager.html +++ b/docs/module-core.EventManager.html @@ -1721,13 +1721,13 @@
diff --git a/docs/module-core.GUI.html b/docs/module-core.GUI.html index 4027105..8ea2548 100644 --- a/docs/module-core.GUI.html +++ b/docs/module-core.GUI.html @@ -247,7 +247,7 @@
Source:
@@ -319,7 +319,7 @@
Source:
@@ -1011,13 +1011,13 @@ the dictionary remains unchanged.


diff --git a/docs/module-core.KeyPress.html b/docs/module-core.KeyPress.html index 9b29a29..ef8a904 100644 --- a/docs/module-core.KeyPress.html +++ b/docs/module-core.KeyPress.html @@ -250,13 +250,13 @@
diff --git a/docs/module-core.Keyboard.html b/docs/module-core.Keyboard.html index 02c8d15..81fc27d 100644 --- a/docs/module-core.Keyboard.html +++ b/docs/module-core.Keyboard.html @@ -1371,13 +1371,13 @@ waitRelease = false, key presses without a corresponding key release will have a
diff --git a/docs/module-core.Logger.html b/docs/module-core.Logger.html index f675dbd..8d9aef5 100644 --- a/docs/module-core.Logger.html +++ b/docs/module-core.Logger.html @@ -795,13 +795,13 @@ See https://github.com/nodeca/pako for details.


diff --git a/docs/module-core.MinimalStim.html b/docs/module-core.MinimalStim.html index 771b22d..283de5c 100644 --- a/docs/module-core.MinimalStim.html +++ b/docs/module-core.MinimalStim.html @@ -1083,13 +1083,13 @@
diff --git a/docs/module-core.Mouse.html b/docs/module-core.Mouse.html index e314c04..cc243be 100644 --- a/docs/module-core.Mouse.html +++ b/docs/module-core.Mouse.html @@ -1733,13 +1733,13 @@ call to getPos
diff --git a/docs/module-core.PsychoJS.html b/docs/module-core.PsychoJS.html index a29d1dc..270532d 100644 --- a/docs/module-core.PsychoJS.html +++ b/docs/module-core.PsychoJS.html @@ -251,7 +251,7 @@
Source:
@@ -460,6 +460,29 @@ + + + + + + + + PAUSED + + + + + +Symbol + + + + + + + + + @@ -506,6 +529,29 @@ + + + + + + + + ERROR + + + + + +Symbol + + + + + + + + + @@ -545,7 +591,7 @@
Source:
@@ -607,7 +653,7 @@
Source:
@@ -687,7 +733,7 @@
Source:
@@ -847,7 +893,7 @@
Source:
@@ -937,171 +983,7 @@
Source:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

(async) downloadResources(resourcesopt)

- - - - - - -
- Synchronously download resources for the experiment. - -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDefaultDescription
resources - - -Array.<{name: string, path: string}> - - - - - - <optional>
- - - - - -
- - [] - - the list of resources
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
@@ -1189,7 +1071,7 @@
Source:
@@ -1352,7 +1234,7 @@ the top level variable (e.g. window) as well.
Source:
@@ -1719,7 +1601,7 @@ before flipping
Source:
@@ -1995,7 +1877,7 @@ that he or she needs to wait for a bit.

Source:
@@ -2145,7 +2027,7 @@ that he or she needs to wait for a bit.

Source:
@@ -2328,7 +2210,7 @@ that he or she needs to wait for a bit.

Source:
@@ -2488,7 +2370,7 @@ that he or she needs to wait for a bit.

Source:
@@ -2533,6 +2415,17 @@ that he or she needs to wait for a bit.

Start the experiment. + +

The resources are specified in the following fashion: +

+

@@ -2825,7 +2718,174 @@ that he or she needs to wait for a bit.

Source:
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

waitForResources(resourcesopt)

+ + + + + + +
+ Block the experiment until the specified resources have been downloaded. + +

Note: only those resources that have not already been downloaded at that point are +considered.

+ +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
resources + + +Array.<{name: string, path: string}> + + + + + + <optional>
+ + + + + +
+ + [] + + the list of resources
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
@@ -2871,13 +2931,13 @@ that he or she needs to wait for a bit.


diff --git a/docs/module-core.ServerManager.html b/docs/module-core.ServerManager.html index 7515305..37f7bf5 100644 --- a/docs/module-core.ServerManager.html +++ b/docs/module-core.ServerManager.html @@ -50,9 +50,8 @@
-

This manager handles all communications between the experiment running in the participant's browser and the remote PsychoJS manager running on the pavlovia.org server, in an asynchronous manner.

-

It is responsible for reading the configuration file of an experiment, for opening and closing a session, for listing and downloading resources, and for uploading results and log.

-

Note: The Server Manager uses Promises to deal with asynchronicity, is mostly called by PsychoJS, and is not exposed to the experiment code.

+

This manager handles all communications between the experiment running in the participant's browser and the pavlovia.org server, in an asynchronous manner.

+

It is responsible for reading the configuration file of an experiment, for opening and closing a session, for listing and downloading resources, and for uploading results, logs, and audio recordings.

@@ -366,7 +365,79 @@
Source:
+ + + + + + + +
+ + + + + + + + +

(readonly) ResourceStatus :Symbol

+ + + + +
+ Resource status +
+ + + +
Type:
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
@@ -510,7 +581,7 @@
Source:
@@ -532,6 +603,145 @@

Methods

+ + + + + + +

(protected) _downloadResources(resources)

+ + + + + + +
+ Download the specified resources. + +

Note: we use the preloadjs library.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
resources + + +Set + + + + a set of names of previously registered resources
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + @@ -698,7 +908,7 @@
Source:
@@ -753,171 +963,6 @@ - - - - - - -

downloadResources(resourcesopt)

- - - - - - -
- Asynchronously download resources for the experiment and register them with the server manager. - - -
- - - - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeAttributesDefaultDescription
resources - - -Array.<{name: string, path: string}> - - - - - - <optional>
- - - - - -
- - [] - - the list of resources
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - - - - - - - - - - - - - @@ -1025,7 +1070,7 @@
Source:
@@ -1083,7 +1128,7 @@ -

getResource(name) → {Object}

+

getResource(name, errorIfNotDownloadedopt) → {Object}

@@ -1102,6 +1147,251 @@ +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
name + + +string + + + + + + + + + + + + name of the requested resource
errorIfNotDownloaded + + +boolean + + + + + + <optional>
+ + + + + +
+ + false + + whether or not to throw an exception if the +resource status is not DOWNLOADED
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + +
Throws:
+ + + +
+
+
+ exception if no resource with that name has previously been registered +
+
+
+
+
+
+ Type +
+
+ +Object.<string, *> + + +
+
+
+
+
+ + + + + +
Returns:
+ + +
+ value of the resource, or undefined if the resource has been registered +but not downloaded yet. +
+ + + +
+
+ Type +
+
+ +Object + + +
+
+ + + + + + + + + + + + + +

getResourceStatus(name) → {core.ServerManager.ResourceStatus}

+ + + + + + +
+ Get the status of a resource. +
+ + + + + + + + +
Parameters:
@@ -1184,7 +1474,7 @@
Source:
@@ -1242,7 +1532,7 @@
- value of the resource + status of the resource
@@ -1253,7 +1543,7 @@
-Object +core.ServerManager.ResourceStatus
@@ -1323,7 +1613,7 @@
Source:
@@ -1375,6 +1665,173 @@ + + + + + + +

prepareResources(resourcesopt)

+ + + + + + +
+ Prepare resources for the experiment: register them with the server manager and possibly +start downloading them right away. + + +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
resources + + +Array.<({name: string, path: string, download: boolean}|Symbol)> + + + + + + <optional>
+ + + + + +
+ + [] + + the list of resources
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + @@ -1433,7 +1890,7 @@
Source:
@@ -1543,7 +2000,7 @@
Source:
@@ -1573,6 +2030,188 @@ + + + + + + +

uploadAudio(audioBlob, tag) → {Promise.<ServerManager.UploadDataPromise>}

+ + + + + + +
+ Asynchronously upload audio data to the pavlovia server. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
audioBlob + + +Blob + + + + the audio blob to be uploaded
tag + + +string + + + + additional tag
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ the response +
+ + + +
+
+ Type +
+
+ +Promise.<ServerManager.UploadDataPromise> + + +
+
+ + + + + + + @@ -1587,7 +2226,7 @@
- Asynchronously upload experiment data to the remote PsychoJS manager. + Asynchronously upload experiment data to the pavlovia server.
@@ -1770,7 +2409,7 @@
Source:
@@ -1836,7 +2475,7 @@
- Asynchronously upload experiment logs to the remote PsychoJS manager. + Asynchronously upload experiment logs to the pavlovia server.
@@ -1984,7 +2623,7 @@
Source:
@@ -2036,6 +2675,163 @@ + + + + + + +

waitForResources(resourcesopt)

+ + + + + + +
+ Block the experiment until the specified resources have been downloaded. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
resources + + +Array.<{name: string, path: string}> + + + + + + <optional>
+ + + + + +
+ + [] + + the list of resources
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + @@ -2052,13 +2848,13 @@
diff --git a/docs/module-core.Window.html b/docs/module-core.Window.html index 7247bf9..8643cd1 100644 --- a/docs/module-core.Window.html +++ b/docs/module-core.Window.html @@ -2039,13 +2039,13 @@ Window.
diff --git a/docs/module-core.WindowMixin.html b/docs/module-core.WindowMixin.html index 9816348..7549aff 100644 --- a/docs/module-core.WindowMixin.html +++ b/docs/module-core.WindowMixin.html @@ -817,13 +817,13 @@
diff --git a/docs/module-core.html b/docs/module-core.html index 9dfe150..d0deb76 100644 --- a/docs/module-core.html +++ b/docs/module-core.html @@ -114,13 +114,13 @@
diff --git a/docs/module-data.ExperimentHandler.html b/docs/module-data.ExperimentHandler.html index 1e3f7ae..b4aa38f 100644 --- a/docs/module-data.ExperimentHandler.html +++ b/docs/module-data.ExperimentHandler.html @@ -1381,13 +1381,13 @@ will be associated with the next trial.
diff --git a/docs/module-data.TrialHandler.html b/docs/module-data.TrialHandler.html index 35d2443..c486d7a 100644 --- a/docs/module-data.TrialHandler.html +++ b/docs/module-data.TrialHandler.html @@ -672,7 +672,7 @@
Source:
@@ -734,7 +734,7 @@
Source:
@@ -977,7 +977,7 @@ It can be a single integer, an array of indices, or a string to be parsed, e.g.:
Source:
@@ -1168,7 +1168,7 @@ It can be a single integer, an array of indices, or a string to be parsed, e.g.:
Source:
@@ -1328,7 +1328,7 @@ It can be a single integer, an array of indices, or a string to be parsed, e.g.:
Source:
@@ -1551,7 +1551,7 @@ and consequently consider only the attributes of the first trial.

Source:
@@ -1661,7 +1661,7 @@ and consequently consider only the attributes of the first trial.

Source:
@@ -1841,7 +1841,7 @@ and consequently consider only the attributes of the first trial.

Source:
@@ -2023,7 +2023,7 @@ and consequently consider only the attributes of the first trial.

Source:
@@ -2140,7 +2140,7 @@ number of trial remaining).
Source:
@@ -2307,7 +2307,7 @@ number of trial remaining).
Source:
@@ -2420,7 +2420,7 @@ number of trial remaining).
Source:
@@ -2579,7 +2579,7 @@ number of trial remaining).
Source:
@@ -2721,13 +2721,13 @@ for (const thisTrial of handler) { console.log(thisTrial); }
diff --git a/docs/module-data.html b/docs/module-data.html index 243f02c..2d5a69c 100644 --- a/docs/module-data.html +++ b/docs/module-data.html @@ -52,6 +52,9 @@
ExperimentHandler
+
Shelf
+
+
TrialHandler
@@ -411,13 +414,13 @@
diff --git a/docs/module-sound.Sound.html b/docs/module-sound.Sound.html index f1fff9d..e1446dd 100644 --- a/docs/module-sound.Sound.html +++ b/docs/module-sound.Sound.html @@ -2159,13 +2159,13 @@ Repeat calls to play may results in the sounds being played on top of each other
diff --git a/docs/module-sound.SoundPlayer.html b/docs/module-sound.SoundPlayer.html index c4fe9ce..5a9197f 100644 --- a/docs/module-sound.SoundPlayer.html +++ b/docs/module-sound.SoundPlayer.html @@ -1033,13 +1033,13 @@
diff --git a/docs/module-sound.TonePlayer.html b/docs/module-sound.TonePlayer.html index 3f94854..2ab04c2 100644 --- a/docs/module-sound.TonePlayer.html +++ b/docs/module-sound.TonePlayer.html @@ -1513,13 +1513,13 @@ we throw an exception.


diff --git a/docs/module-sound.TrackPlayer.html b/docs/module-sound.TrackPlayer.html index d6a0224..b452f0c 100644 --- a/docs/module-sound.TrackPlayer.html +++ b/docs/module-sound.TrackPlayer.html @@ -1607,13 +1607,13 @@
diff --git a/docs/module-sound.html b/docs/module-sound.html index b9cc7b8..b425b7d 100644 --- a/docs/module-sound.html +++ b/docs/module-sound.html @@ -49,6 +49,12 @@

Classes

+
AudioClip
+
+ +
Microphone
+
+
Sound
@@ -57,6 +63,12 @@
TrackPlayer
+ +
Transcriber
+
+ +
Transcript
+
@@ -90,13 +102,13 @@
diff --git a/docs/module-util.Clock.html b/docs/module-util.Clock.html index 03bcd56..e9dd0ee 100644 --- a/docs/module-util.Clock.html +++ b/docs/module-util.Clock.html @@ -480,13 +480,13 @@ smaller). As a consequence, getTime() may return a negative number.


diff --git a/docs/module-util.Color.html b/docs/module-util.Color.html index 6971619..8ba164e 100644 --- a/docs/module-util.Color.html +++ b/docs/module-util.Color.html @@ -1951,13 +1951,13 @@ static methods for converting colors from one space to another.


diff --git a/docs/module-util.ColorMixin.html b/docs/module-util.ColorMixin.html index 095a4ad..e6883f1 100644 --- a/docs/module-util.ColorMixin.html +++ b/docs/module-util.ColorMixin.html @@ -672,13 +672,13 @@
diff --git a/docs/module-util.CountdownTimer.html b/docs/module-util.CountdownTimer.html index e8de316..a21b5ce 100644 --- a/docs/module-util.CountdownTimer.html +++ b/docs/module-util.CountdownTimer.html @@ -652,13 +652,13 @@ to newTime
diff --git a/docs/module-util.EventEmitter.html b/docs/module-util.EventEmitter.html index e780dc6..e104681 100644 --- a/docs/module-util.EventEmitter.html +++ b/docs/module-util.EventEmitter.html @@ -994,13 +994,13 @@ observable.emit("change", { a: 1 });
diff --git a/docs/module-util.MixinBuilder.html b/docs/module-util.MixinBuilder.html index 5baced5..1d72f66 100644 --- a/docs/module-util.MixinBuilder.html +++ b/docs/module-util.MixinBuilder.html @@ -215,13 +215,13 @@ class NewClass extends mix(BaseClass).with(Mixin1, Mixin2) { ... }
diff --git a/docs/module-util.MonotonicClock.html b/docs/module-util.MonotonicClock.html index f0345fc..7257aa1 100644 --- a/docs/module-util.MonotonicClock.html +++ b/docs/module-util.MonotonicClock.html @@ -740,13 +740,13 @@
diff --git a/docs/module-util.PsychObject.html b/docs/module-util.PsychObject.html index b3f7305..5bb8094 100644 --- a/docs/module-util.PsychObject.html +++ b/docs/module-util.PsychObject.html @@ -362,7 +362,7 @@ It is responsible for handling attributes.

-

(protected) _addAttribute(name, value, defaultValue, onChange)

+

(protected) _addAttribute(name, value, defaultValueopt, onChangeopt)

@@ -394,6 +394,8 @@ It is responsible for handling attributes.

Type + Attributes + @@ -419,6 +421,14 @@ It is responsible for handling attributes.

+ + + + + + + + @@ -442,6 +452,14 @@ It is responsible for handling attributes.

+ + + + + + + + @@ -465,6 +483,16 @@ It is responsible for handling attributes.

+ + + <optional>
+ + + + + + + @@ -488,6 +516,16 @@ It is responsible for handling attributes.

+ + + <optional>
+ + + + + + + @@ -1016,13 +1054,13 @@ was not previously set)
diff --git a/docs/module-util.Scheduler.html b/docs/module-util.Scheduler.html index 5e87a77..43b88b4 100644 --- a/docs/module-util.Scheduler.html +++ b/docs/module-util.Scheduler.html @@ -945,13 +945,13 @@ task would be by calling scheduler.add(
diff --git a/docs/module-util.html b/docs/module-util.html index 275585c..0aa7d11 100644 --- a/docs/module-util.html +++ b/docs/module-util.html @@ -353,6 +353,167 @@ https://stackoverflow.com/a/9851769

+

(static) extensionFromMimeType(mimeType) → {string}

+ + + + + + +
+ Return the file extension corresponding to an audio mime type. +If the provided mimeType is not a string (e.g. null, undefined, an array) +or unknown, then '.dat' is returned, instead of throwing an exception. +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
mimeType + + +string + + + + the MIME type, e.g. 'audio/webm;codecs=opus'
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ the corresponding file extension, e.g. '.webm' +
+ + + +
+
+ Type +
+
+ +string + + +
+
+ + + + + + + + + + + + +

(static) flattenArray(array) → {Array.<Object>}

@@ -5197,13 +5358,13 @@ missing the naught prefix, and is able to process several arrays, e.g. "[1,2][3,
diff --git a/docs/module-visual.Form.html b/docs/module-visual.Form.html index 7ff8215..d6000e4 100644 --- a/docs/module-visual.Form.html +++ b/docs/module-visual.Form.html @@ -2192,13 +2192,13 @@ This is typically called in the constructor of a stimulus, when attributes are a
diff --git a/docs/module-visual.ImageStim.html b/docs/module-visual.ImageStim.html index 4286070..14dd2c4 100644 --- a/docs/module-visual.ImageStim.html +++ b/docs/module-visual.ImageStim.html @@ -1291,13 +1291,13 @@
diff --git a/docs/module-visual.MovieStim.html b/docs/module-visual.MovieStim.html index ebf6349..f373872 100644 --- a/docs/module-visual.MovieStim.html +++ b/docs/module-visual.MovieStim.html @@ -1088,13 +1088,13 @@
diff --git a/docs/module-visual.Polygon.html b/docs/module-visual.Polygon.html index 6633183..5f0286f 100644 --- a/docs/module-visual.Polygon.html +++ b/docs/module-visual.Polygon.html @@ -1021,13 +1021,13 @@
diff --git a/docs/module-visual.Rect.html b/docs/module-visual.Rect.html index 4690bac..e6e174a 100644 --- a/docs/module-visual.Rect.html +++ b/docs/module-visual.Rect.html @@ -1027,13 +1027,13 @@
diff --git a/docs/module-visual.ShapeStim.html b/docs/module-visual.ShapeStim.html index c39dc6d..479b243 100644 --- a/docs/module-visual.ShapeStim.html +++ b/docs/module-visual.ShapeStim.html @@ -1243,13 +1243,13 @@ This is overridden in order to provide a finer inclusion test.
diff --git a/docs/module-visual.Slider.html b/docs/module-visual.Slider.html index b2d7cdb..ade7872 100644 --- a/docs/module-visual.Slider.html +++ b/docs/module-visual.Slider.html @@ -2720,13 +2720,13 @@ This is typically called in the constructor of a stimulus, when attributes are a
diff --git a/docs/module-visual.TextBox.html b/docs/module-visual.TextBox.html index 0b82167..1f3c67c 100644 --- a/docs/module-visual.TextBox.html +++ b/docs/module-visual.TextBox.html @@ -1616,13 +1616,13 @@
diff --git a/docs/module-visual.TextStim.html b/docs/module-visual.TextStim.html index 1243e62..5c97d71 100644 --- a/docs/module-visual.TextStim.html +++ b/docs/module-visual.TextStim.html @@ -1384,13 +1384,13 @@ unlike getSize().
diff --git a/docs/module-visual.VisualStim.html b/docs/module-visual.VisualStim.html index 318fd85..882eec4 100644 --- a/docs/module-visual.VisualStim.html +++ b/docs/module-visual.VisualStim.html @@ -1408,13 +1408,13 @@ This is typically called in the constructor of a stimulus, when attributes are a
diff --git a/docs/module-visual.html b/docs/module-visual.html index 334a026..180dc6d 100644 --- a/docs/module-visual.html +++ b/docs/module-visual.html @@ -104,13 +104,13 @@
diff --git a/docs/sound_Sound.js.html b/docs/sound_Sound.js.html index dd0b44b..577f51c 100644 --- a/docs/sound_Sound.js.html +++ b/docs/sound_Sound.js.html @@ -290,13 +290,13 @@ export class Sound extends PsychObject
diff --git a/docs/sound_SoundPlayer.js.html b/docs/sound_SoundPlayer.js.html index 473851c..e71a1e9 100644 --- a/docs/sound_SoundPlayer.js.html +++ b/docs/sound_SoundPlayer.js.html @@ -197,13 +197,13 @@ export class SoundPlayer extends PsychObject
diff --git a/docs/sound_TonePlayer.js.html b/docs/sound_TonePlayer.js.html index 6043c48..e8815c7 100644 --- a/docs/sound_TonePlayer.js.html +++ b/docs/sound_TonePlayer.js.html @@ -422,13 +422,13 @@ TonePlayer.SoundLibrary = {
diff --git a/docs/sound_TrackPlayer.js.html b/docs/sound_TrackPlayer.js.html index 28d5b6d..27d3cab 100644 --- a/docs/sound_TrackPlayer.js.html +++ b/docs/sound_TrackPlayer.js.html @@ -264,13 +264,13 @@ export class TrackPlayer extends SoundPlayer
diff --git a/docs/util_Clock.js.html b/docs/util_Clock.js.html index a249812..10f0548 100644 --- a/docs/util_Clock.js.html +++ b/docs/util_Clock.js.html @@ -260,13 +260,13 @@ export class CountdownTimer extends Clock
diff --git a/docs/util_Color.js.html b/docs/util_Color.js.html index 5cd7c49..55a6bb2 100644 --- a/docs/util_Color.js.html +++ b/docs/util_Color.js.html @@ -753,13 +753,13 @@ Color.NAMED_COLORS = {
diff --git a/docs/util_ColorMixin.js.html b/docs/util_ColorMixin.js.html index 3056d8a..5e7ba73 100644 --- a/docs/util_ColorMixin.js.html +++ b/docs/util_ColorMixin.js.html @@ -115,13 +115,13 @@ export let ColorMixin = (superclass) => class extends superclass
diff --git a/docs/util_EventEmitter.js.html b/docs/util_EventEmitter.js.html index 33297e8..183d319 100644 --- a/docs/util_EventEmitter.js.html +++ b/docs/util_EventEmitter.js.html @@ -193,13 +193,13 @@ export class EventEmitter
diff --git a/docs/util_PsychObject.js.html b/docs/util_PsychObject.js.html index 3a28d4e..0dfe197 100644 --- a/docs/util_PsychObject.js.html +++ b/docs/util_PsychObject.js.html @@ -377,10 +377,10 @@ export class PsychObject extends EventEmitter * @protected * @param {string} name - the name of the attribute * @param {object} value - the value of the attribute - * @param {object} defaultValue - the default value for the attribute - * @param {function} onChange - function called upon changes to the attribute value + * @param {object} [defaultValue] - the default value for the attribute + * @param {function} [onChange] - function called upon changes to the attribute value */ - _addAttribute(name, value, defaultValue, onChange = () => {}) + _addAttribute(name, value, defaultValue = undefined, onChange = () => {}) { const getPropertyName = 'get' + name[0].toUpperCase() + name.substr(1); if (typeof this[getPropertyName] === 'undefined') @@ -445,13 +445,13 @@ export class PsychObject extends EventEmitter
diff --git a/docs/util_Scheduler.js.html b/docs/util_Scheduler.js.html index dce25e7..17f7792 100644 --- a/docs/util_Scheduler.js.html +++ b/docs/util_Scheduler.js.html @@ -162,10 +162,10 @@ export class Scheduler * @name module:util.Scheduler#start * @public */ - start() + async start() { const self = this; - let update = (timestamp) => + const update = async (timestamp) => { // stop the animation if need be: if (self._stopAtNextUpdate) @@ -177,7 +177,7 @@ export class Scheduler // self._psychoJS.window._writeLogOnFlip(); // run the next scheduled tasks until a scene render is requested: - const state = self._runNextTasks(); + const state = await self._runNextTasks(); if (state === Scheduler.Event.QUIT) { self._status = Scheduler.Status.STOPPED; @@ -223,7 +223,7 @@ export class Scheduler * @private * @return {module:util.Scheduler#Event} the state of the scheduler after the last task ran */ - _runNextTasks() + async _runNextTasks() { this._status = Scheduler.Status.RUNNING; @@ -262,14 +262,14 @@ export class Scheduler // if the current task is a function, we run it: if (this._currentTask instanceof Function) { - state = this._currentTask(...this._currentArgs); + state = await this._currentTask(...this._currentArgs); } // otherwise, we assume that the current task is a scheduler and we run its tasks until a rendering // of the scene is required. // note: "if (this._currentTask instanceof Scheduler)" does not work because of CORS... else { - state = this._currentTask._runNextTasks(); + state = await this._currentTask._runNextTasks(); if (state === Scheduler.Event.QUIT) { // if the experiment has not ended, we move onto the next task: @@ -356,13 +356,13 @@ Scheduler.Status = {
diff --git a/docs/util_Util.js.html b/docs/util_Util.js.html index 3dd1eac..6aa6b39 100644 --- a/docs/util_Util.js.html +++ b/docs/util_Util.js.html @@ -986,12 +986,12 @@ export function offerDataForDownload(filename, data, type) } else { - let elem = window.document.createElement('a'); - elem.href = window.URL.createObjectURL(blob); - elem.download = filename; - document.body.appendChild(elem); - elem.click(); - document.body.removeChild(elem); + const anchor = document.createElement('a'); + anchor.href = window.URL.createObjectURL(blob); + anchor.download = filename; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); } } @@ -1122,6 +1122,43 @@ export function sum(inputMaybe = []) // Add up each successive entry starting from naught .reduce(add, 0); } + + +/** + * Return the file extension corresponding to an audio mime type. + * If the provided mimeType is not a string (e.g. null, undefined, an array) + * or unknown, then '.dat' is returned, instead of throwing an exception. + * + * @name module:util.extensionFromMimeType + * @function + * @public + * @param {string} mimeType the MIME type, e.g. 'audio/webm;codecs=opus' + * @return {string} the corresponding file extension, e.g. '.webm' + */ +export function extensionFromMimeType(mimeType) +{ + if (typeof mimeType !== 'string') + { + return '.dat'; + } + + if (mimeType.indexOf('audio/webm') === 0) + { + return '.webm'; + } + + if (mimeType.indexOf('audio/ogg') === 0) + { + return '.ogg'; + } + + if (mimeType.indexOf('audio/wav') === 0) + { + return '.wav'; + } + + return '.dat'; +} @@ -1132,13 +1169,13 @@ export function sum(inputMaybe = [])
diff --git a/docs/visual_Form.js.html b/docs/visual_Form.js.html index 2306c35..10cf284 100644 --- a/docs/visual_Form.js.html +++ b/docs/visual_Form.js.html @@ -1176,13 +1176,13 @@ Form._defaultItems = {
diff --git a/docs/visual_ImageStim.js.html b/docs/visual_ImageStim.js.html index 7cff959..9dda506 100644 --- a/docs/visual_ImageStim.js.html +++ b/docs/visual_ImageStim.js.html @@ -395,13 +395,13 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
diff --git a/docs/visual_MovieStim.js.html b/docs/visual_MovieStim.js.html index bcd1a7a..5f4473c 100644 --- a/docs/visual_MovieStim.js.html +++ b/docs/visual_MovieStim.js.html @@ -475,13 +475,13 @@ export class MovieStim extends VisualStim
diff --git a/docs/visual_Polygon.js.html b/docs/visual_Polygon.js.html index 49729ba..3a74b10 100644 --- a/docs/visual_Polygon.js.html +++ b/docs/visual_Polygon.js.html @@ -181,13 +181,13 @@ export class Polygon extends ShapeStim
diff --git a/docs/visual_Rect.js.html b/docs/visual_Rect.js.html index 31fff73..2b6c526 100644 --- a/docs/visual_Rect.js.html +++ b/docs/visual_Rect.js.html @@ -186,13 +186,13 @@ export class Rect extends ShapeStim
diff --git a/docs/visual_ShapeStim.js.html b/docs/visual_ShapeStim.js.html index 9a94a4d..2f5627b 100644 --- a/docs/visual_ShapeStim.js.html +++ b/docs/visual_ShapeStim.js.html @@ -423,13 +423,13 @@ ShapeStim.KnownShapes = {
diff --git a/docs/visual_Slider.js.html b/docs/visual_Slider.js.html index bce79b1..e047f73 100644 --- a/docs/visual_Slider.js.html +++ b/docs/visual_Slider.js.html @@ -1399,13 +1399,13 @@ Slider.Skin = {
diff --git a/docs/visual_TextBox.js.html b/docs/visual_TextBox.js.html index ff031f9..8eb3a56 100644 --- a/docs/visual_TextBox.js.html +++ b/docs/visual_TextBox.js.html @@ -557,13 +557,13 @@ TextBox._defaultSizeMap = new Map([
diff --git a/docs/visual_TextInput.js.html b/docs/visual_TextInput.js.html index 2580905..f5c9373 100644 --- a/docs/visual_TextInput.js.html +++ b/docs/visual_TextInput.js.html @@ -889,13 +889,13 @@ function DefaultBoxGenerator(styles)
diff --git a/docs/visual_TextStim.js.html b/docs/visual_TextStim.js.html index e106175..cb7a375 100644 --- a/docs/visual_TextStim.js.html +++ b/docs/visual_TextStim.js.html @@ -459,13 +459,13 @@ TextStim._defaultWrapWidthMap = new Map([
diff --git a/docs/visual_VisualStim.js.html b/docs/visual_VisualStim.js.html index 4efbe76..31e7caf 100644 --- a/docs/visual_VisualStim.js.html +++ b/docs/visual_VisualStim.js.html @@ -341,13 +341,13 @@ export class VisualStim extends util.mix(MinimalStim).with(WindowMixin)
diff --git a/index.css b/index.css new file mode 100644 index 0000000..2208889 --- /dev/null +++ b/index.css @@ -0,0 +1,149 @@ + +/* project and resource dialogs */ +label, input, select { + display: block; + padding-bottom: .5em; +} + +input.text, select.text { + margin-bottom: 1em; + width: 95%; + padding: .5em; +} + +fieldset { + padding: 0; + border: 0; + margin-top: 1em; +} + +a, a:active, a:focus, a:visited { + outline: 0; + color: #007EB7; +} + +a:hover { + color: #000000; +} + +.progress { + padding: .5em 0 .5em 0; +} + +.logo { + display: block; + margin-left: auto; + margin-right: auto; + max-width: 100%; + margin-bottom: 1em; +} + + +/* don't display close button in the top right corner of the box */ +.no-close .ui-dialog-titlebar-close { + display: none; +} + +.ui-dialog-content { + margin-top: 1em; +} + + +/* for mobile phones only */ +@media only screen and (max-width: 1080px) { + + .ui-widget { + -ms-transform: scale(2); + -webkit-transform: scale(2); + transform: scale(2); + } + + .ui-widget .ui-progressbar { + -ms-transform: scale(1); + -webkit-transform: scale(1); + transform: scale(1); + } + + .ui-dialog .ui-dialog-buttonpane { + padding-top: 1em; + } + + .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button { + -ms-transform: scale(1); + -webkit-transform: scale(1); + transform: scale(1); + } + + .ui-dialog .ui-dialog-titlebar { + padding: 1em 2em; + } + + .ui-dialog-titlebar .ui-button { + margin-right: 1em; + } + + .ui-dialog-titlebar .ui-dialog-titlebar-close { + -ms-transform: scale(1); + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@media only screen and (max-width: 1080px) and (orientation:landscape) { + + .ui-widget { + -ms-transform: scale(1.5); + -webkit-transform: scale(1.5); + transform: scale(1.5); + } + + .ui-widget .ui-progressbar { + -ms-transform: scale(1); + -webkit-transform: scale(1); + transform: scale(1); + } + + .ui-dialog .ui-dialog-buttonpane { + padding-top: 1em; + } + + .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset .ui-button { + -ms-transform: scale(1); + -webkit-transform: scale(1); + transform: scale(1); + } + + .ui-dialog .ui-dialog-titlebar { + padding: 1em 2em; + } + + .ui-dialog-titlebar .ui-button { + margin-right: 1em; + } + + .ui-dialog-titlebar .ui-dialog-titlebar-close { + -ms-transform: scale(1); + -webkit-transform: scale(1); + transform: scale(1); + } +} + + +/* Initialisation message (which will disappear behind the canvas) */ +#root:after { + content: "initialising the experiment..."; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +/* Initialisation message for IE11 */ +@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + #root:after { + content: "initialising the experiment... | Internet Explorer / Edge [beta]"; + + color: #A05000; + font-weight: bold; + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..d484753 --- /dev/null +++ b/index.js @@ -0,0 +1,5 @@ +export * as util from './js/util/index.js'; +export * as core from './js/core/index.js'; +export * as data from './js/data/index.js'; +export * as visual from './js/visual/index.js'; +export * as sound from './js/sound/index.js'; diff --git a/package.json b/package.json index fd6c55c..e8091a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psychojs", - "version": "2021.1.4", + "version": "2021.x", "private": true, "description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments", "license": "MIT", @@ -49,7 +49,6 @@ ] } }, - "dependencies": {}, "devDependencies": { "@babel/core": "^7.12.3", "@babel/preset-env": "^7.12.1", diff --git a/src/core/GUI.js b/src/core/GUI.js index 9964c1a..7bdb57b 100644 --- a/src/core/GUI.js +++ b/src/core/GUI.js @@ -313,7 +313,7 @@ export class GUI //$.blockUI({ message: "", baseZ: 1}); // show dialog box: - $("#progressbar").progressbar({value: self._progressBarCurrentIncrement}); + $("#progressbar").progressbar({value: self._progressBarCurrentValue}); $("#progressbar").progressbar("option", "max", self._progressBarMax); } @@ -587,15 +587,14 @@ export class GUI { this._psychoJS.logger.debug('signal: ' + util.toString(signal)); - // all resources have been registered: - if (signal.message === ServerManager.Event.RESOURCES_REGISTERED) + // the download of the specified resources has started: + if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCES) { // for each resource, we have a 'downloading resource' and a 'resource downloaded' message: this._progressBarMax = signal.count * 2; $("#progressbar").progressbar("option", "max", this._progressBarMax); - this._progressBarCurrentIncrement = 0; - $("#progressMsg").text('all resources registered.'); + this._progressBarCurrentValue = 0; } // all the resources have been downloaded: show the ok button @@ -607,23 +606,25 @@ export class GUI } // update progress bar: - else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE || signal.message === ServerManager.Event.RESOURCE_DOWNLOADED) + else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE + || signal.message === ServerManager.Event.RESOURCE_DOWNLOADED) { - if (typeof this._progressBarCurrentIncrement === 'undefined') + if (typeof this._progressBarCurrentValue === 'undefined') { - this._progressBarCurrentIncrement = 0; + this._progressBarCurrentValue = 0; } - ++this._progressBarCurrentIncrement; + ++this._progressBarCurrentValue; if (signal.message === ServerManager.Event.RESOURCE_DOWNLOADED) { - $("#progressMsg").text('downloaded ' + this._progressBarCurrentIncrement / 2 + ' / ' + this._progressBarMax / 2); + $("#progressMsg").text('downloaded ' + (this._progressBarCurrentValue / 2) + ' / ' + (this._progressBarMax / 2)); + } + else + { + $("#progressMsg").text('downloading ' + (this._progressBarCurrentValue / 2) + ' / ' + (this._progressBarMax / 2)); } // $("#progressMsg").text(signal.resource + ': downloaded.'); - // else - // $("#progressMsg").text(signal.resource + ': downloading...'); - - $("#progressbar").progressbar("option", "value", this._progressBarCurrentIncrement); + $("#progressbar").progressbar("option", "value", this._progressBarCurrentValue); } // unknown message: we just display it diff --git a/src/core/Keyboard.js b/src/core/Keyboard.js index 338fc8b..170854a 100644 --- a/src/core/Keyboard.js +++ b/src/core/Keyboard.js @@ -198,7 +198,7 @@ export class Keyboard extends PsychObject const keyEvent = this._circularBuffer[i]; if (keyEvent && keyEvent.status === Keyboard.KeyStatus.KEY_UP) { - // check that the key is in the keyList: + // if the keylist is empty of the key is in the keyList: if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey)) { // look for a corresponding, preceding keydown event: diff --git a/src/core/Logger.js b/src/core/Logger.js index 880a789..7de2bfb 100644 --- a/src/core/Logger.js +++ b/src/core/Logger.js @@ -312,9 +312,9 @@ export class Logger */ _customConsoleLayout() { - const detectedBrowser = this._psychoJS.browser; + const detectedBrowser = util.detectBrowser(); - const customLayout = new log4javascript.PatternLayout("%p %f{1} | %m"); + const customLayout = new log4javascript.PatternLayout("%p %d{HH:mm:ss.SSS} %f{1} | %m"); customLayout.setCustomField('location', function (layout, loggingReference) { // we throw a fake exception to retrieve the stack trace @@ -345,7 +345,7 @@ export class Logger const file = buf[buf.length - 3].split('/').pop(); const method = relevantEntry.split('@')[0]; - return method + ' ' + file + ' ' + line; + return method + ' ' + file + ':' + line; } else if (detectedBrowser === 'Safari') { @@ -363,7 +363,7 @@ export class Logger const line = buf.pop(); const file = buf.pop().split('/').pop(); - return method + ' ' + file + ' ' + line; + return method + ' ' + file + ':' + line; } else { diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index c3b26b2..62725fc 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -18,6 +18,7 @@ import {GUI} from './GUI'; import {MonotonicClock} from '../util/Clock'; import {Logger} from './Logger'; import * as util from '../util/Util'; +// import {Shelf} from "../data/Shelf"; /** @@ -30,6 +31,7 @@ import * as util from '../util/Util'; */ export class PsychoJS { + /** * Properties */ @@ -109,6 +111,11 @@ export class PsychoJS return this._browser; } + // get shelf() + // { + // return this._shelf; + // } + /** * @constructor @@ -154,6 +161,9 @@ export class PsychoJS // Window: this._window = undefined; + // // Shelf: + // this._shelf = new Shelf(this); + // redirection URLs: this._cancellationUrl = undefined; this._completionUrl = undefined; @@ -292,6 +302,17 @@ export class PsychoJS /** * Start the experiment. * + *

The resources are specified in the following fashion: + *

+ *

+ * * @param {Object} options * @param {string} [options.configURL=config.json] - the URL of the configuration file * @param {string} [options.expName=UNKNOWN] - the name of the experiment @@ -299,8 +320,6 @@ export class PsychoJS * @param {Array.<{name: string, path: string}>} [resources=[]] - the list of resources * @async * @public - * - * @todo: close session on window or tab close */ async start({configURL = 'config.json', expName = 'UNKNOWN', expInfo = {}, resources = []} = {}) { @@ -386,11 +405,11 @@ export class PsychoJS // start the asynchronous download of resources: - this._serverManager.downloadResources(resources); + await this._serverManager.prepareResources(resources); // start the experiment: this.logger.info('[PsychoJS] Start Experiment.'); - this._scheduler.start(); + await this._scheduler.start(); } catch (error) { @@ -400,8 +419,12 @@ export class PsychoJS } + /** - * Synchronously download resources for the experiment. + * Block the experiment until the specified resources have been downloaded. + * + *

Note: only those resources that have not already been downloaded at that point are + * considered.

* *