1
0
mirror of https://github.com/psychopy/psychojs.git synced 2025-05-10 02:30:53 +00:00

Merge pull request #567 from apitiot/2023.2.0

2023.2.0
This commit is contained in:
Alain Pitiot 2023-07-19 16:21:51 +02:00 committed by GitHub
commit 36e6f8131a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1191 additions and 238 deletions

1002
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "psychojs",
"version": "2022.3.1",
"version": "2023.2.0",
"private": true,
"description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments",
"license": "MIT",
@ -34,6 +34,7 @@
"howler": "^2.2.1",
"log4javascript": "github:Ritzlgrmft/log4javascript",
"pako": "^1.0.10",
"pixi-filters": "^5.0.0",
"pixi.js-legacy": "^6.0.4",
"seedrandom": "^3.0.5",
"tone": "^14.7.77",

View File

@ -302,7 +302,13 @@ export class EventManager
{
const timestamp = MonotonicClock.getReferenceTime();
let code = event.code;
// Note: we are using event.key since we are interested in the input character rather than
// the physical key position on the keyboard, i.e. we need to take into account the keyboard
// layout
// See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for a comment regarding
// event.code's lack of suitability
let code = EventManager._pygletMap[event.key];
// let code = event.code;
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
if (typeof code === "undefined")

View File

@ -50,6 +50,9 @@ export class GUI
{
this._psychoJS = psychoJS;
// info fields excluded from the GUI:
this._excludedInfo = {};
// gui listens to RESOURCE events from the server manager:
psychoJS.serverManager.on(ServerManager.Event.RESOURCE, (signal) =>
{
@ -87,9 +90,6 @@ export class GUI
requireParticipantClick = GUI.DEFAULT_SETTINGS.DlgFromDict.requireParticipantClick
})
{
// get info from URL:
const infoFromUrl = util.getUrlParameters();
this._progressBarMax = 0;
this._allResourcesDownloaded = false;
this._requiredKeys = [];
@ -113,6 +113,19 @@ export class GUI
self._dialogComponent.tStart = t;
self._dialogComponent.status = PsychoJS.Status.STARTED;
// prepare the info fields excluded from the GUI, including those from the URL:
const excludedInfo = {};
for (let key in self._excludedInfo)
{
excludedInfo[key.trim().toLowerCase()] = self._excludedInfo[key];
}
const infoFromUrl = util.getUrlParameters();
infoFromUrl.forEach((value, key) =>
{
excludedInfo[key.trim().toLowerCase()] = value;
});
// if the experiment is licensed, and running on the license rather than on credit,
// we use the license logo:
if (self._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER
@ -130,7 +143,13 @@ export class GUI
markup += "<div class='dialog-content'>";
// alert title and close button:
markup += `<div id='experiment-dialog-title' class='dialog-title'><p>${title}</p><button id='dialogClose' class='dialog-close' data-a11y-dialog-hide aria-label='Cancel Experiment'>&times;</button></div>`;
markup += "<div id='experiment-dialog-title' class='dialog-title'>";
markup += `<p>${title}</p>`;
markup += "<button id='dialogClose' class='dialog-close' data-a11y-dialog-hide aria-label='Cancel Experiment'>&times;</button>";
markup += "</div>";
// everything above the buttons is in a scrollable container:
markup += "<div class='scrollable-container'>";
// logo, if need be:
if (typeof logoUrl === "string")
@ -139,14 +158,16 @@ export class GUI
}
// add a combobox or text areas for each entry in the dictionary:
let atLeastOneIncludedKey = false;
Object.keys(dictionary).forEach((key, keyIdx) =>
{
const value = dictionary[key];
const keyId = "form-input-" + keyIdx;
// only create an input if the key is not in the URL:
let inUrl = false;
const cleanedDictKey = key.trim().toLowerCase();
const isIncluded = !(cleanedDictKey in excludedInfo);
/*let inUrl = false;
infoFromUrl.forEach((urlValue, urlKey) =>
{
const cleanedUrlKey = urlKey.trim().toLowerCase();
@ -155,10 +176,13 @@ export class GUI
inUrl = true;
// break;
}
});
});*/
if (!inUrl)
if (isIncluded)
// if (!inUrl)
{
atLeastOneIncludedKey = true;
markup += `<label for='${keyId}'> ${key} </label>`;
// if the field is required:
@ -185,7 +209,7 @@ export class GUI
markup += "</select>";
}
// otherwise we use a single string input:
// otherwise we use a single string input:
//if (typeof value === 'string')
else
{
@ -199,17 +223,27 @@ export class GUI
markup += "<p class='validateTips'>Fields marked with an asterisk (*) are required.</p>";
}
markup += "</div>"; // scrollable-container
// separator, if need be:
if (atLeastOneIncludedKey)
{
markup += "<hr>";
}
// progress bar:
markup += `<hr><div id='progressMsg' class='progress-msg'>${self._progressMessage}</div>`;
markup += `<div id='progressMsg' class='progress-msg'>${self._progressMessage}</div>`;
markup += "<div class='progress-container'><div id='progressBar' class='progress-bar'></div></div>";
// buttons:
markup += "<hr>";
markup += "<div class='dialog-button-group'>";
markup += "<button id='dialogCancel' class='dialog-button' aria-label='Cancel Experiment'>Cancel</button>";
if (self._requireParticipantClick)
{
markup += "<button id='dialogOK' class='dialog-button disabled' aria-label='Start Experiment'>Ok</button>";
}
markup += "</div>"; // button-group
markup += "</div></div>";
@ -346,14 +380,18 @@ export class GUI
{
const error = this._userFriendlyError(errorCode);
markup += `<div id='experiment-dialog-title' class='dialog-title ${error.class}'><p>${error.title}</p></div>`;
markup += "<div class='scrollable-container'>";
markup += `<p>${error.text}</p>`;
markup += "</div>";
}
else
{
markup += `<div id='experiment-dialog-title' class='dialog-title dialog-error'><p>Error</p></div>`;
markup += "<div class='scrollable-container'>";
markup += `<p>Unfortunately we encountered the following error:</p>`;
markup += stackCode;
markup += "<p>Try to run the experiment again. If the error persists, contact the experiment designer.</p>";
markup += "</div>";
}
}
@ -361,27 +399,36 @@ export class GUI
else if (typeof warning !== "undefined")
{
markup += `<div id='experiment-dialog-title' class='dialog-title dialog-warning'><p>Warning</p></div>`;
markup += "<div class='scrollable-container'>";
markup += `<p>${warning}</p>`;
markup += "</div>";
}
// we are displaying a message:
else if (typeof message !== "undefined")
{
markup += `<div id='experiment-dialog-title' class='dialog-title'><p>Message</p></div>`;
markup += "<div id='experiment-dialog-title' class='dialog-title'><p>Message</p></div>";
markup += "<div class='scrollable-container'>";
markup += `<p>${message}</p>`;
markup += "</div>";
}
if (showOK || showCancel)
{
markup += "<hr>";
}
if (showCancel)
if (showCancel || showOK)
{
markup += "<button id='dialogCancel' class='dialog-button' aria-label='Close dialog'>Cancel</button>";
}
if (showOK)
{
markup += "<button id='dialogOK' class='dialog-button' aria-label='Close dialog'>Ok</button>";
markup += "<div class='button-group'>";
if (showCancel)
{
markup += "<button id='dialogCancel' class='dialog-button' aria-label='Close dialog'>Cancel</button>";
}
if (showOK)
{
markup += "<button id='dialogOK' class='dialog-button' aria-label='Close dialog'>Ok</button>";
}
markup += "</div>"; // button-group
}
markup += "</div></div>";

View File

@ -354,7 +354,13 @@ export class Keyboard extends PsychObject
*/
self._previousKeydownKey = event.key;
let code = event.code;
// Note: we are using event.key since we are interested in the input character rather than
// the physical key position on the keyboard, i.e. we need to take into account the keyboard
// layout
// See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code for a comment regarding
// event.code's lack of suitability
let code = EventManager._pygletMap[event.key];
// let code = event.code;
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
if (typeof code === "undefined")
@ -394,7 +400,9 @@ export class Keyboard extends PsychObject
self._previousKeydownKey = undefined;
let code = event.code;
// Note: see above for explanation regarding the use of event.key in lieu of event.code
let code = EventManager._pygletMap[event.key];
// let code = event.code;
// take care of legacy Microsoft Edge:
if (typeof code === "undefined")

View File

@ -530,6 +530,7 @@ export class PsychoJS
const response = { origin: "PsychoJS.quit", context: "when terminating the experiment" };
this._experiment.experimentEnded = true;
this._experiment.isCompleted = isCompleted;
this.status = PsychoJS.Status.STOPPED;
const isServerEnv = (this.getEnvironment() === ExperimentHandler.Environment.SERVER);
@ -601,7 +602,7 @@ export class PsychoJS
if (showOK)
{
let text = "Thank you for your patience.<br/><br/>";
let text = "Thank you for your patience.";
text += (typeof message !== "undefined") ? message : "Goodbye!";
this._gui.dialog({
message: text,

View File

@ -1293,7 +1293,7 @@ export class ServerManager extends PsychObject
}
// font files:
else if (["ttf", "otf", "woff", "woff2"].indexOf(pathExtension) > -1)
else if (["ttf", "otf", "woff", "woff2","eot"].indexOf(pathExtension) > -1)
{
fontResources.push(name);
}

View File

@ -13,6 +13,7 @@ import { MonotonicClock } from "../util/Clock.js";
import { Color } from "../util/Color.js";
import { PsychObject } from "../util/PsychObject.js";
import { Logger } from "./Logger.js";
import { hasTouchScreen } from "../util/Util.js";
/**
* <p>Window displays the various stimuli of the experiment.</p>
@ -181,7 +182,7 @@ export class Window extends PsychObject
{
// gets updated frame by frame
const lastDelta = this.psychoJS.scheduler._lastDelta;
const fps = lastDelta === 0 ? 60.0 : 1000 / lastDelta;
const fps = (lastDelta === 0) ? 60.0 : (1000.0 / lastDelta);
return fps;
}
@ -493,6 +494,17 @@ export class Window extends PsychObject
// update the renderer size and the Window's stimuli whenever the browser's size or orientation change:
this._resizeCallback = (e) =>
{
// if the user device is a mobile phone or tablet (we use the presence of a touch screen as a
// proxy), we need to detect whether the change in size is due to the appearance of a virtual keyboard
// in which case we do not want to resize the canvas. This is rather tricky and so we resort to
// the below trick. It would be better to use the VirtualKeyboard API, but it is not widely
// available just yet, as of 2023-06.
const keyboardHeight = 300;
if (hasTouchScreen() && (window.screen.height - window.visualViewport.height) > keyboardHeight)
{
return;
}
Window._resizePixiRenderer(this, e);
this._backgroundSprite.width = this._size[0];
this._backgroundSprite.height = this._size[1];

View File

@ -276,6 +276,7 @@ export class ExperimentHandler extends PsychObject
}
let data = this._trialsData;
// if the experiment data have to be cleared, we first make a copy of them:
if (clear)
{
@ -351,6 +352,19 @@ export class ExperimentHandler extends PsychObject
}
}
/**
* Get the results of the experiment as a .csv string, ready to be uploaded or stored.
*
* @return {string} a .csv representation of the experiment results.
*/
getResultAsCsv()
{
// note: we use the XLSX library as it automatically deals with header, takes care of quotes,
// newlines, etc.
const worksheet = XLSX.utils.json_to_sheet(this._trialsData);
return "\ufeff" + XLSX.utils.sheet_to_csv(worksheet);
}
/**
* Get the attribute names and values for the current trial of a given loop.
* <p> Only info relating to the trial execution are returned.</p>

View File

@ -26,13 +26,12 @@ body {
/* Project and resource dialogs */
.dialog-container label,
.dialog-container input,
.dialog-container select {
box-sizing: border-box;
display: block;
padding-bottom: 0.5em;
box-sizing: border-box;
display: block;
padding-bottom: 0.5em;
}
.dialog-container input.text,
@ -40,6 +39,13 @@ body {
margin-bottom: 1em;
padding: 0.5em;
width: 100%;
height: 34px;
border: 1px solid #767676;
border-radius: 2px;
background: #ffffff;
color: #333;
font-size: 14px;
}
.dialog-container fieldset {
@ -71,12 +77,19 @@ body {
}
.dialog-content {
display: flex;
flex-direction: column;
row-gap: 0;
margin: auto;
z-index: 2;
position: relative;
width: 500px;
max-width: 88vw;
/*max-height: 90vh;*/
max-height: 93%;
padding: 0.5em;
border-radius: 2px;
@ -88,11 +101,24 @@ body {
box-shadow: 1px 1px 3px #555555;
}
.dialog-content .scrollable-container {
height: 100%;
padding: 0 0.5em;
overflow-x: hidden;
overflow-y: auto;
}
.dialog-content hr {
width: 100%;
}
.dialog-title {
padding: 0.5em;
margin-bottom: 1em;
background-color: #009900;
background-color: #00dd00;
/*background-color: #009900;*/
border-radius: 2px;
}
@ -111,6 +137,11 @@ body {
}
.dialog-close {
display: flex;
justify-content: center;
align-items: center;
line-height: 1.1em;
position: absolute;
top: 0.7em;
right: 0.7em;
@ -153,7 +184,7 @@ body {
.dialog-button {
padding: 0.5em 1em 0.5em 1em;
margin: 0.5em 0.5em 0.5em 0;
/*margin: 0.5em 0.5em 0.5em 0;*/
border: 1px solid #555555;
border-radius: 2px;
@ -176,6 +207,14 @@ body {
border: 1px solid #000000;
}
.dialog-button-group {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
column-gap: 0.5em;
}
.disabled {
border: 1px solid #AAAAAA;
color: #AAAAAA;
@ -186,10 +225,15 @@ body {
}
.logo {
display: block;
display: flex;
flex: 0 1 auto;
height: 100%;
width: auto;
/*display: block;
margin: 0 auto 1em;
max-height: 20vh;
max-width: 100%;
max-width: 100%;*/
}
a,

View File

@ -117,9 +117,12 @@ export class Scheduler
* Start this scheduler.
*
* <p>Note: tasks are run after each animation frame.</p>
*
* @return {Promise<void>} a promise resolved when the scheduler stops, e.g. when the experiments finishes
*/
start()
{
let shedulerResolve;
const self = this;
const update = async (timestamp) =>
{
@ -127,6 +130,7 @@ export class Scheduler
if (self._stopAtNextUpdate)
{
self._status = Scheduler.Status.STOPPED;
shedulerResolve();
return;
}
@ -137,6 +141,7 @@ export class Scheduler
if (state === Scheduler.Event.QUIT)
{
self._status = Scheduler.Status.STOPPED;
shedulerResolve();
return;
}
@ -155,6 +160,12 @@ export class Scheduler
// start the animation:
requestAnimationFrame(update);
// return a promise resolved when the scheduler is stopped:
return new Promise((resolve, _) =>
{
shedulerResolve = resolve;
});
}
/**

View File

@ -647,6 +647,11 @@ export function toString(object)
return object.toString();
}
if (typeof object === "function")
{
return `<function ${object.name}>`;
}
try
{
const symbolReplacer = (key, value) =>
@ -1473,6 +1478,47 @@ export function loadCss(cssId, cssPath)
}
}
/**
* Whether the user device has a touchscreen, e.g. it is a mobile phone or tablet.
*
* @return {boolean} true if the user device has a touchscreen.
* @note the code below is directly adapted from MDN
*/
export function hasTouchScreen()
{
let hasTouchScreen = false;
if ("maxTouchPoints" in navigator)
{
hasTouchScreen = navigator.maxTouchPoints > 0;
}
else if ("msMaxTouchPoints" in navigator)
{
hasTouchScreen = navigator.msMaxTouchPoints > 0;
}
else
{
const mQ = matchMedia?.("(pointer:coarse)");
if (mQ?.media === "(pointer:coarse)")
{
hasTouchScreen = !!mQ.matches;
}
else if ("orientation" in window)
{
hasTouchScreen = true;
}
else
{
const UA = navigator.userAgent;
hasTouchScreen =
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
}
}
return hasTouchScreen;
}
/**
* Enum that stores possible text directions.
* Note that Arabic is the same as RTL but added here to support PsychoPy's

View File

@ -9,6 +9,7 @@
import { Mouse } from "../core/Mouse.js";
import { TextBox } from "./TextBox.js";
import * as util from "../util/Util";
/**
* <p>ButtonStim visual stimulus.</p>
@ -32,6 +33,7 @@ export class ButtonStim extends TextBox
* @param {Color} [options.borderColor= Color("white")] the border color
* @param {Color} [options.borderWidth= 0] the border width
* @param {number} [options.opacity= 1.0] - the opacity
* @param {number} [options.depth= 0] - the depth (i.e. the z order)
* @param {number} [options.letterHeight= undefined] - the height of the text
* @param {boolean} [options.bold= true] - whether or not the text is bold
* @param {boolean} [options.italic= false] - whether or not the text is italic
@ -54,11 +56,14 @@ export class ButtonStim extends TextBox
borderColor,
borderWidth = 0,
opacity,
depth,
letterHeight,
bold = true,
italic,
autoDraw,
autoLog,
boxFn,
multiline
} = {},
)
{
@ -77,12 +82,15 @@ export class ButtonStim extends TextBox
borderColor,
borderWidth,
opacity,
depth,
letterHeight,
multiline,
bold,
italic,
alignment: "center",
autoDraw,
autoLog,
boxFn
});
this.psychoJS.logger.debug("create a new Button with name: ", name);
@ -112,7 +120,7 @@ export class ButtonStim extends TextBox
if (this._autoLog)
{
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`);
}
}

View File

@ -47,7 +47,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
* @param {boolean} [options.autoLog= false] - whether or not to log
*/
constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, autoDraw, autoLog } = {})
constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, aspectRatio, autoDraw, autoLog } = {})
{
super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog });
@ -94,6 +94,12 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
false,
this._onChange(false, false),
);
this._addAttribute(
"aspectRatio",
aspectRatio,
ImageStim.AspectRatioStrategy.VARIABLE,
this._onChange(true, true),
);
// estimate the bounding box:
this._estimateBoundingBox();
@ -309,7 +315,18 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image, texOpts));
}
this._pixi = PIXI.Sprite.from(this._texture);
if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING)
{
const [width_px, _] = util.to_px([this.size[0], 0], this.units, this.win);
this._pixi = PIXI.TilingSprite.from(this._texture, 1, 1);
this._pixi.width = width_px;
this._pixi.height = this._texture.height;
}
else
{
this._pixi = PIXI.Sprite.from(this._texture);
}
// add a mask if need be:
if (typeof this._mask !== "undefined")
@ -349,8 +366,24 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
// set the scale:
const displaySize = this._getDisplaySize();
const size_px = util.to_px(displaySize, this.units, this.win);
const scaleX = size_px[0] / this._texture.width;
const scaleY = size_px[1] / this._texture.height;
let scaleX = size_px[0] / this._texture.width;
let scaleY = size_px[1] / this._texture.height;
if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_WIDTH)
{
scaleY = scaleX;
}
else if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_HEIGHT)
{
scaleX = scaleY;
}
else if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING)
{
scaleX = 1.0;
scaleY = 1.0;
}
// note: this calls VisualStim.setAnchor, which properly sets the PixiJS anchor
// from the PsychoPy text format
this.anchor = this._anchor;
this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
@ -383,7 +416,47 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
displaySize = util.to_unit(textureSize, "pix", this.win, this.units);
}
}
else
{
if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_WIDTH)
{
// use the size of the texture, if we have access to it:
if (typeof this._texture !== "undefined" && this._texture.width > 0)
{
displaySize = [displaySize[0], displaySize[0] * this._texture.height / this._texture.width];
}
}
else if (this.aspectRatio === ImageStim.AspectRatioStrategy.FIT_TO_HEIGHT)
{
// use the size of the texture, if we have access to it:
if (typeof this._texture !== "undefined" && this._texture.width > 0)
{
displaySize = [displaySize[1] * this._texture.width / this._texture.height, displaySize[1]];
}
}
else if (this.aspectRatio === ImageStim.AspectRatioStrategy.HORIZONTAL_TILING)
{
// use the size of the texture, if we have access to it:
if (typeof this._texture !== "undefined" && this._texture.width > 0)
{
displaySize = [displaySize[0], this._texture.height];
}
}
}
return displaySize;
}
}
/**
* ImageStim Aspect Ratio Strategy.
*
* @enum {Symbol}
* @readonly
*/
ImageStim.AspectRatioStrategy = {
FIT_TO_WIDTH: Symbol.for("FIT_TO_WIDTH"),
HORIZONTAL_TILING: Symbol.for("HORIZONTAL_TILING"),
FIT_TO_HEIGHT: Symbol.for("FIT_TO_HEIGHT"),
VARIABLE: Symbol.for("VARIABLE"),
};

View File

@ -86,7 +86,8 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
clipMask,
autoDraw,
autoLog,
fitToContent
fitToContent,
boxFn
} = {},
)
{
@ -202,12 +203,14 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
// and setSize called from super class would not have a proper effect
this.setSize(size);
this._addAttribute("boxFn", boxFn, null);
// estimate the bounding box:
this._estimateBoundingBox();
if (this._autoLog)
{
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`);
}
}
@ -481,6 +484,26 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
alignmentStyles = ["center", "center"];
}
let box;
if (this._boxFn !== null)
{
box = this._boxFn;
}
else
{
// note: box style properties eventually become PIXI.Graphics settings, so same syntax applies
box = {
fill: new Color(this._fillColor).int,
alpha: this._fillColor === undefined || this._fillColor === null ? 0 : 1,
rounded: 5,
stroke: {
color: new Color(this._borderColor).int,
width: borderWidth_px,
alpha: this._borderColor === undefined || this._borderColor === null ? 0 : 1
}
};
}
return {
// input style properties eventually become CSS, so same syntax applies
input: {
@ -504,41 +527,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
overflow: "hidden",
pointerEvents: "none"
},
// box style properties eventually become PIXI.Graphics settings, so same syntax applies
box: {
fill: new Color(this._fillColor).int,
alpha: this._fillColor === undefined || this._fillColor === null ? 0 : 1,
rounded: 5,
stroke: {
color: new Color(this._borderColor).int,
width: borderWidth_px,
alpha: this._borderColor === undefined || this._borderColor === null ? 0 : 1
},
/*default: {
fill: new Color(this._fillColor).int,
rounded: 5,
stroke: {
color: new Color(this._borderColor).int,
width: borderWidth_px
}
},
focused: {
fill: new Color(this._fillColor).int,
rounded: 5,
stroke: {
color: new Color(this._borderColor).int,
width: borderWidth_px
}
},
disabled: {
fill: new Color(this._fillColor).int,
rounded: 5,
stroke: {
color: new Color(this._borderColor).int,
width: borderWidth_px
}
}*/
},
box
};
}

View File

@ -95,18 +95,11 @@ class MaxDiffMatrix
question.setCssRoot(rootClass);
question.cssClasses.mainRoot = rootClass;
}
let html;
let headerCells = "";
let subHeaderCells = "";
let bodyCells = "";
let bodyHTML = "";
let cellGenerator;
let i, j;
// Relying on a fact that there's always 2 columns.
// This is correct according current Qualtrics design for MaxDiff matrices.
// Header generation
headerCells =
let headerCells =
`<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[0].text}</th>
<td></td>
<td></td>
@ -114,9 +107,10 @@ class MaxDiffMatrix
<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[1].text}</th>`;
// Body generation
for (i = 0; i < question.rows.length; i++)
let bodyHTML = "";
for (let i = 0; i < question.rows.length; i++)
{
bodyCells =
const bodyCells =
`<td class="${CSS_CLASSES.TABLE_CELL}">
<label class="${CSS_CLASSES.LABEL}">
<input type="radio" class="${CSS_CLASSES.ITEM_VALUE}" name="${question.rows[i].value}" data-column=${question.columns[0].value}>
@ -135,7 +129,7 @@ class MaxDiffMatrix
bodyHTML += `<tr class="${CSS_CLASSES.TABLE_ROW}">${bodyCells}</tr>`;
}
html = `<table class="${CSS_CLASSES.TABLE}">
let html = `<table class="${CSS_CLASSES.TABLE}">
<thead>
<tr>${headerCells}</tr>
</thead>
@ -147,14 +141,15 @@ class MaxDiffMatrix
let inputDOMS = el.querySelectorAll("input");
for (i = 0; i < inputDOMS.length; i++)
for (let i = 0; i < inputDOMS.length; i++)
{
inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput);
}
}
}
export default function init (Survey) {
export default function init (Survey)
{
var widget = {
//the widget name. It should be unique and written in lowcase.
name: "maxdiffmatrix",