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

merge with 2023.2.0

This commit is contained in:
lightest 2023-07-21 18:27:24 +01:00
commit 71069244cb
32 changed files with 2285 additions and 1539 deletions

View File

@ -53,7 +53,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: '14' node-version: '15'
- name: Cache modules psychojs_testing - name: Cache modules psychojs_testing
uses: actions/cache@v2 uses: actions/cache@v2
env: env:

View File

@ -45,7 +45,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: '12' node-version: '15'
# START: install psychojs_testing # START: install psychojs_testing
- name: Checkout psychojs_testing - name: Checkout psychojs_testing

1002
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -302,7 +302,13 @@ export class EventManager
{ {
const timestamp = MonotonicClock.getReferenceTime(); 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): // take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
if (typeof code === "undefined") if (typeof code === "undefined")

View File

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

View File

@ -354,7 +354,13 @@ export class Keyboard extends PsychObject
*/ */
self._previousKeydownKey = event.key; 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): // take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
if (typeof code === "undefined") if (typeof code === "undefined")
@ -394,7 +400,9 @@ export class Keyboard extends PsychObject
self._previousKeydownKey = undefined; 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: // take care of legacy Microsoft Edge:
if (typeof code === "undefined") if (typeof code === "undefined")

View File

@ -530,6 +530,7 @@ export class PsychoJS
const response = { origin: "PsychoJS.quit", context: "when terminating the experiment" }; const response = { origin: "PsychoJS.quit", context: "when terminating the experiment" };
this._experiment.experimentEnded = true; this._experiment.experimentEnded = true;
this._experiment.isCompleted = isCompleted;
this.status = PsychoJS.Status.STOPPED; this.status = PsychoJS.Status.STOPPED;
const isServerEnv = (this.getEnvironment() === ExperimentHandler.Environment.SERVER); const isServerEnv = (this.getEnvironment() === ExperimentHandler.Environment.SERVER);
@ -601,7 +602,7 @@ export class PsychoJS
if (showOK) if (showOK)
{ {
let text = "Thank you for your patience.<br/><br/>"; let text = "Thank you for your patience.";
text += (typeof message !== "undefined") ? message : "Goodbye!"; text += (typeof message !== "undefined") ? message : "Goodbye!";
this._gui.dialog({ this._gui.dialog({
message: text, message: text,
@ -789,7 +790,7 @@ export class PsychoJS
const self = this; const self = this;
window.onerror = function(message, source, lineno, colno, error) window.onerror = function(message, source, lineno, colno, error)
{console.log('@@@', message) {
// check for ResizeObserver loop limit exceeded error: // check for ResizeObserver loop limit exceeded error:
// ref: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded // ref: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
if (message === "ResizeObserver loop limit exceeded" || if (message === "ResizeObserver loop limit exceeded" ||

View File

@ -314,6 +314,34 @@ export class ServerManager extends PsychObject
return pathStatusData.data; return pathStatusData.data;
} }
/**
* Release a resource.
*
* @param {string} name - the name of the resource to release
* @return {boolean} true if a resource with the given name was previously registered with the manager,
* false otherwise.
*/
releaseResource(name)
{
const response = {
origin: "ServerManager.releaseResource",
context: "when releasing resource: " + name,
};
const pathStatusData = this._resources.get(name);
if (typeof pathStatusData === "undefined")
{
return false;
}
// TODO check the current status: prevent the release of a resources currently downloading
this._psychoJS.logger.debug(`releasing resource: ${name}`);
this._resources.delete(name);
return true;
}
/** /**
* Get the status of a single resource or the reduced status of an array of resources. * Get the status of a single resource or the reduced status of an array of resources.
* *
@ -506,18 +534,18 @@ export class ServerManager extends PsychObject
// pre-process the resources: // pre-process the resources:
for (let r = 0; r < resources.length; ++r) for (let r = 0; r < resources.length; ++r)
{ {
const resource = resources[r];
// convert those resources that are only a string to an object with name and path: // convert those resources that are only a string to an object with name and path:
if (typeof resource === "string") if (typeof resources[r] === "string")
{ {
resources[r] = { resources[r] = {
name: resource, name: resources[r],
path: resource, path: resources[r],
download: true download: true
}; };
} }
const resource = resources[r];
// deal with survey models: // deal with survey models:
if ("surveyId" in resource) if ("surveyId" in resource)
{ {
@ -1265,7 +1293,7 @@ export class ServerManager extends PsychObject
} }
// font files: // font files:
else if (["ttf", "otf", "woff", "woff2"].indexOf(pathExtension) > -1) else if (["ttf", "otf", "woff", "woff2","eot"].indexOf(pathExtension) > -1)
{ {
fontResources.push(name); fontResources.push(name);
} }

View File

@ -13,6 +13,7 @@ import { MonotonicClock } from "../util/Clock.js";
import { Color } from "../util/Color.js"; import { Color } from "../util/Color.js";
import { PsychObject } from "../util/PsychObject.js"; import { PsychObject } from "../util/PsychObject.js";
import { Logger } from "./Logger.js"; import { Logger } from "./Logger.js";
import { hasTouchScreen } from "../util/Util.js";
/** /**
* <p>Window displays the various stimuli of the experiment.</p> * <p>Window displays the various stimuli of the experiment.</p>
@ -181,7 +182,7 @@ export class Window extends PsychObject
{ {
// gets updated frame by frame // gets updated frame by frame
const lastDelta = this.psychoJS.scheduler._lastDelta; 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; 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: // update the renderer size and the Window's stimuli whenever the browser's size or orientation change:
this._resizeCallback = (e) => 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); Window._resizePixiRenderer(this, e);
this._backgroundSprite.width = this._size[0]; this._backgroundSprite.width = this._size[0];
this._backgroundSprite.height = this._size[1]; this._backgroundSprite.height = this._size[1];

View File

@ -276,6 +276,7 @@ export class ExperimentHandler extends PsychObject
} }
let data = this._trialsData; let data = this._trialsData;
// if the experiment data have to be cleared, we first make a copy of them: // if the experiment data have to be cleared, we first make a copy of them:
if (clear) 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. * 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> * <p> Only info relating to the trial execution are returned.</p>

View File

@ -13,7 +13,7 @@ body {
/* Initialisation message (which will disappear behind the canvas) */ /* Initialisation message (which will disappear behind the canvas) */
#root::after { #root::after {
content: "initialising the experiment..."; content: "initialising...";
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
@ -26,13 +26,12 @@ body {
/* Project and resource dialogs */ /* Project and resource dialogs */
.dialog-container label, .dialog-container label,
.dialog-container input, .dialog-container input,
.dialog-container select { .dialog-container select {
box-sizing: border-box; box-sizing: border-box;
display: block; display: block;
padding-bottom: 0.5em; padding-bottom: 0.5em;
} }
.dialog-container input.text, .dialog-container input.text,
@ -40,6 +39,13 @@ body {
margin-bottom: 1em; margin-bottom: 1em;
padding: 0.5em; padding: 0.5em;
width: 100%; width: 100%;
height: 34px;
border: 1px solid #767676;
border-radius: 2px;
background: #ffffff;
color: #333;
font-size: 14px;
} }
.dialog-container fieldset { .dialog-container fieldset {
@ -71,12 +77,19 @@ body {
} }
.dialog-content { .dialog-content {
display: flex;
flex-direction: column;
row-gap: 0;
margin: auto; margin: auto;
z-index: 2; z-index: 2;
position: relative; position: relative;
width: 500px; width: 500px;
max-width: 88vw; max-width: 88vw;
/*max-height: 90vh;*/
max-height: 93%;
padding: 0.5em; padding: 0.5em;
border-radius: 2px; border-radius: 2px;
@ -88,11 +101,24 @@ body {
box-shadow: 1px 1px 3px #555555; 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 { .dialog-title {
padding: 0.5em; padding: 0.5em;
margin-bottom: 1em; margin-bottom: 1em;
background-color: #009900; background-color: #00dd00;
/*background-color: #009900;*/
border-radius: 2px; border-radius: 2px;
} }
@ -111,6 +137,11 @@ body {
} }
.dialog-close { .dialog-close {
display: flex;
justify-content: center;
align-items: center;
line-height: 1.1em;
position: absolute; position: absolute;
top: 0.7em; top: 0.7em;
right: 0.7em; right: 0.7em;
@ -153,7 +184,7 @@ body {
.dialog-button { .dialog-button {
padding: 0.5em 1em 0.5em 1em; 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: 1px solid #555555;
border-radius: 2px; border-radius: 2px;
@ -176,6 +207,14 @@ body {
border: 1px solid #000000; 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 { .disabled {
border: 1px solid #AAAAAA; border: 1px solid #AAAAAA;
color: #AAAAAA; color: #AAAAAA;
@ -186,10 +225,15 @@ body {
} }
.logo { .logo {
display: block; display: flex;
flex: 0 1 auto;
height: 100%;
width: auto;
/*display: block;
margin: 0 auto 1em; margin: 0 auto 1em;
max-height: 20vh; max-height: 20vh;
max-width: 100%; max-width: 100%;*/
} }
a, a,

View File

@ -90,6 +90,7 @@ export class MonotonicClock
{ {
// yyyy-mm-dd, hh:mm:ss.sss // yyyy-mm-dd, hh:mm:ss.sss
return MonotonicClock.getDate() return MonotonicClock.getDate()
.replaceAll("/","-")
// yyyy-mm-dd_hh:mm:ss.sss // yyyy-mm-dd_hh:mm:ss.sss
.replace(", ", "_") .replace(", ", "_")
// yyyy-mm-dd_hh[h]mm:ss.sss // yyyy-mm-dd_hh[h]mm:ss.sss

View File

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

View File

@ -322,27 +322,64 @@ export function IsPointInsidePolygon(point, vertices)
} }
/** /**
* Shuffle an array in place using the Fisher-Yastes's modern algorithm * Shuffle an array, or a portion of that array, in place using the Fisher-Yastes's modern algorithm
* <p>See details here: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm</p> * <p>See details here: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm</p>
* *
* @param {Object[]} array - the input 1-D array * @param {Object[]} array - the input 1-D array
* @param {Function} [randomNumberGenerator = undefined] - A function used to generated random numbers in the interal [0, 1). Defaults to Math.random * @param {Function} [randomNumberGenerator= undefined] - A function used to generated random numbers in the interval [0, 1). Defaults to Math.random
* @param [startIndex= undefined] - start index in the array
* @param [endIndex= undefined] - end index in the array
* @return {Object[]} the shuffled array * @return {Object[]} the shuffled array
*/ */
export function shuffle(array, randomNumberGenerator = undefined) export function shuffle(array, randomNumberGenerator = undefined, startIndex = undefined, endIndex = undefined)
{ {
if (randomNumberGenerator === undefined) // if array is not an array, we return it untouched rather than throwing an exception:
if (!array || !Array.isArray(array))
{
return array;
}
if (typeof startIndex === "undefined")
{
startIndex = 0;
}
if (typeof endIndex === "undefined")
{
endIndex = array.length - 1;
}
if (typeof randomNumberGenerator === "undefined")
{ {
randomNumberGenerator = Math.random; randomNumberGenerator = Math.random;
} }
for (let i = array.length - 1; i > 0; i--)
for (let i = endIndex; i > startIndex; i--)
{ {
const j = Math.floor(randomNumberGenerator() * (i + 1)); const j = Math.floor(randomNumberGenerator() * (i + 1));
[array[i], array[j]] = [array[j], array[i]]; [array[i], array[j]] = [array[j], array[i]];
} }
return array; return array;
} }
/**
* linspace
*
* @name module:util.linspace
* @function
* @public
* @param {Object[]} startValue, stopValue, cardinality
* @return {Object[]} an array from startValue to stopValue with cardinality steps
*/
export function linspace(startValue, stopValue, cardinality) {
var arr = [];
var step = (stopValue - startValue) / (cardinality - 1);
for (var i = 0; i < cardinality; i++) {
arr.push(startValue + (step * i));
}
return arr;
}
/** /**
* Pick a random value from an array, uses `util.shuffle` to shuffle the array and returns the last value. * Pick a random value from an array, uses `util.shuffle` to shuffle the array and returns the last value.
* *
@ -610,6 +647,11 @@ export function toString(object)
return object.toString(); return object.toString();
} }
if (typeof object === "function")
{
return `<function ${object.name}>`;
}
try try
{ {
const symbolReplacer = (key, value) => const symbolReplacer = (key, value) =>
@ -1436,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. * Enum that stores possible text directions.
* Note that Arabic is the same as RTL but added here to support PsychoPy's * 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 { Mouse } from "../core/Mouse.js";
import { TextBox } from "./TextBox.js"; import { TextBox } from "./TextBox.js";
import * as util from "../util/Util";
/** /**
* <p>ButtonStim visual stimulus.</p> * <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.borderColor= Color("white")] the border color
* @param {Color} [options.borderWidth= 0] the border width * @param {Color} [options.borderWidth= 0] the border width
* @param {number} [options.opacity= 1.0] - the opacity * @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 {number} [options.letterHeight= undefined] - the height of the text
* @param {boolean} [options.bold= true] - whether or not the text is bold * @param {boolean} [options.bold= true] - whether or not the text is bold
* @param {boolean} [options.italic= false] - whether or not the text is italic * @param {boolean} [options.italic= false] - whether or not the text is italic
@ -54,11 +56,14 @@ export class ButtonStim extends TextBox
borderColor, borderColor,
borderWidth = 0, borderWidth = 0,
opacity, opacity,
depth,
letterHeight, letterHeight,
bold = true, bold = true,
italic, italic,
autoDraw, autoDraw,
autoLog, autoLog,
boxFn,
multiline
} = {}, } = {},
) )
{ {
@ -77,12 +82,15 @@ export class ButtonStim extends TextBox
borderColor, borderColor,
borderWidth, borderWidth,
opacity, opacity,
depth,
letterHeight, letterHeight,
multiline,
bold, bold,
italic, italic,
alignment: "center", alignment: "center",
autoDraw, autoDraw,
autoLog, autoLog,
boxFn
}); });
this.psychoJS.logger.debug("create a new Button with name: ", name); this.psychoJS.logger.debug("create a new Button with name: ", name);
@ -112,7 +120,7 @@ export class ButtonStim extends TextBox
if (this._autoLog) if (this._autoLog)
{ {
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`);
} }
} }

View File

@ -46,6 +46,8 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
* @param {boolean} [options.flipVert= false] - whether or not to flip vertically * @param {boolean} [options.flipVert= false] - whether or not to flip vertically
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @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 * @param {boolean} [options.autoLog= false] - whether or not to log
* @param {ImageStim.AspectRatioStrategy} [options.aspectRatio= ImageStim.AspectRatioStrategy.VARIABLE] - the aspect ratio handling strategy
* @param {number} [options.blurVal= 0] - the blur value. Goes 0 to as hish as you like. 0 is no blur.
*/ */
constructor({ constructor({
name, name,
@ -67,6 +69,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
flipVert, flipVert,
autoDraw, autoDraw,
autoLog, autoLog,
aspectRatio,
blurVal blurVal
} = {}) } = {})
{ {
@ -118,6 +121,12 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
false, false,
this._onChange(false, false), this._onChange(false, false),
); );
this._addAttribute(
"aspectRatio",
aspectRatio,
ImageStim.AspectRatioStrategy.VARIABLE,
this._onChange(true, true),
);
this._addAttribute( this._addAttribute(
"blurVal", "blurVal",
blurVal, blurVal,
@ -366,7 +375,18 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image, texOpts)); 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: // add a mask if need be:
if (typeof this._mask !== "undefined") if (typeof this._mask !== "undefined")
@ -406,8 +426,24 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
// set the scale: // set the scale:
const displaySize = this._getDisplaySize(); const displaySize = this._getDisplaySize();
const size_px = util.to_px(displaySize, this.units, this.win); const size_px = util.to_px(displaySize, this.units, this.win);
const scaleX = size_px[0] / this._texture.width; let scaleX = size_px[0] / this._texture.width;
const scaleY = size_px[1] / this._texture.height; 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.anchor = this._anchor;
this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX; this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
this._pixi.scale.y = this.flipVert ? scaleY : -scaleY; this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
@ -445,7 +481,47 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
displaySize = util.to_unit(textureSize, "pix", this.win, this.units); 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; 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

@ -385,4 +385,29 @@ ShapeStim.KnownShapes = {
[-0.39, 0.31], [-0.39, 0.31],
[-0.09, 0.18], [-0.09, 0.18],
], ],
triangle: [
[+0.0, 0.5], // Point
[-0.5, -0.5], // Bottom left
[+0.5, -0.5], // Bottom right
],
rectangle: [
[-.5, .5], // Top left
[ .5, .5], // Top right
[ .5, -.5], // Bottom left
[-.5, -.5], // Bottom right
],
arrow: [
[0.0, 0.5],
[-0.5, 0.0],
[-1/6, 0.0],
[-1/6, -0.5],
[1/6, -0.5],
[1/6, 0.0],
[0.5, 0.0],
],
}; };
// Alias some names for convenience
ShapeStim.KnownShapes['star'] = ShapeStim.KnownShapes['star7']

File diff suppressed because it is too large Load Diff

View File

@ -65,6 +65,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
opacity, opacity,
depth, depth,
text, text,
placeholder,
font, font,
letterHeight, letterHeight,
bold, bold,
@ -85,7 +86,8 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
clipMask, clipMask,
autoDraw, autoDraw,
autoLog, autoLog,
fitToContent fitToContent,
boxFn
} = {}, } = {},
) )
{ {
@ -98,7 +100,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
); );
this._addAttribute( this._addAttribute(
"placeholder", "placeholder",
text, placeholder,
"", "",
this._onChange(true, true), this._onChange(true, true),
); );
@ -201,12 +203,14 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
// and setSize called from super class would not have a proper effect // and setSize called from super class would not have a proper effect
this.setSize(size); this.setSize(size);
this._addAttribute("boxFn", boxFn, null);
// estimate the bounding box: // estimate the bounding box:
this._estimateBoundingBox(); this._estimateBoundingBox();
if (this._autoLog) if (this._autoLog)
{ {
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`); this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`);
} }
} }
@ -480,6 +484,26 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
alignmentStyles = ["center", "center"]; 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 { return {
// input style properties eventually become CSS, so same syntax applies // input style properties eventually become CSS, so same syntax applies
input: { input: {
@ -503,41 +527,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
overflow: "hidden", overflow: "hidden",
pointerEvents: "none" pointerEvents: "none"
}, },
// box style properties eventually become PIXI.Graphics settings, so same syntax applies box
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
}
}*/
},
}; };
} }

View File

@ -1,307 +0,0 @@
/**
* @desc "MaxDiff" matrix.
* */
class MaxDiffMatrix
{
constructor (cfg = {})
{
// surveyCSS contains css class names provided by the applied theme
// INCLUDING those added/modified by application's code.
const surveyCSS = cfg.question.css;
this._CSS_CLASSES = {
WRAPPER: `${surveyCSS.matrix.tableWrapper} matrix-maxdiff`,
TABLE: surveyCSS.matrix.root,
TABLE_ROW: surveyCSS.matrixdropdown.row,
TABLE_HEADER_CELL: surveyCSS.matrix.headerCell,
TABLE_CELL: surveyCSS.matrix.cell,
INPUT_TEXT: surveyCSS.text.root,
LABEL: surveyCSS.matrix.label,
ITEM_CHECKED: surveyCSS.matrix.itemChecked,
ITEM_VALUE: surveyCSS.matrix.itemValue,
ITEM_DECORATOR: surveyCSS.matrix.materialDecorator,
RADIO: surveyCSS.radiogroup.item,
SELECT: surveyCSS.dropdown.control,
CHECKBOX: surveyCSS.checkbox.item
};
// const CSS_CLASSES = {
// WRAPPER: "sv-matrix matrix-maxdiff",
// TABLE: "sv-table sv-matrix-root",
// TABLE_ROW: "sv-table__row",
// TABLE_HEADER_CELL: "sv-table__cell sv-table__cell--header",
// TABLE_CELL: "sv-table__cell sv-matrix__cell",
// INPUT_TEXT: "sv-text",
// RADIO: "sv-radio",
// SELECT: "sv-dropdown",
// CHECKBOX: "sv-checkbox"
// };
this._question = cfg.question;
this._DOM = cfg.el;
this._DOM.classList.add(...this._CSS_CLASSES.WRAPPER.split(" "));
this._bindedHandlers =
{
_handleInput: this._handleInput.bind(this)
};
this._init(this._question, this._DOM);
}
_handleInput (e)
{
const valueCoordinates = e.currentTarget.name.split("-");
const row = valueCoordinates[0];
const col = parseInt(e.currentTarget.dataset.column, 10);
const colRadioDOMS = this._DOM.querySelectorAll(`input[data-column="${col}"]`);
if (this._question.value === undefined)
{
this._question.value = {};
}
const oldVal = this._question.value;
const newVal = {[row]: col};
// Handle case when exclusiveAnswer option is false?
let inputRow;
let i;
for (i = 0; i < colRadioDOMS.length; i++)
{
if (colRadioDOMS[i] !== e.currentTarget)
{
colRadioDOMS[i].checked = false;
inputRow = colRadioDOMS[i].name;
// Preserving previously ticked columns within other rows
if (oldVal[inputRow] !== undefined && oldVal[inputRow] !== col)
{
newVal[inputRow] = oldVal[inputRow];
}
}
}
this._question.value = newVal;
console.log(row, col, this._question.value);
}
_init (question, el)
{
let t = performance.now();
const CSS_CLASSES = this._CSS_CLASSES;
if (question.css.matrix.mainRoot)
{
// Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior
const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`;
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 =
`<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[0].text}</th>
<td></td>
<td></td>
<td></td>
<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[1].text}</th>`;
// Body generation
for (i = 0; i < question.rows.length; i++)
{
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}>
<span class="${CSS_CLASSES.ITEM_DECORATOR}"></span>
</label>
</td>
<td></td>
<td class="${CSS_CLASSES.TABLE_CELL}">${question.rows[i].text}</td>
<td></td>
<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[1].value}>
<span class="${CSS_CLASSES.ITEM_DECORATOR}"></span>
</label>
</td>`;
bodyHTML += `<tr class="${CSS_CLASSES.TABLE_ROW}">${bodyCells}</tr>`;
}
html = `<table class="${CSS_CLASSES.TABLE}">
<thead>
<tr>${headerCells}</tr>
</thead>
<tbody>${bodyHTML}</tbody>
</table>`;
console.log("maxdiff matrix generation took", performance.now() - t);
el.insertAdjacentHTML("beforeend", html);
let inputDOMS = el.querySelectorAll("input");
for (i = 0; i < inputDOMS.length; i++)
{
inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput);
}
}
}
export default function init (Survey) {
var widget = {
//the widget name. It should be unique and written in lowcase.
name: "maxdiffmatrix",
//the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder
title: "MaxDiff matrix",
//the name of the icon on the toolbox. We will leave it empty to use the standard one
iconName: "",
//If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded
widgetIsLoaded: function () {
//return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page
return true; //we do not require anything so we just return true.
},
//SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior
isFit: function (question) {
//we return true if the type of question is maxdiffmatrix
return question.getType() === 'maxdiffmatrix';
//the following code will activate the widget for a text question with inputType equals to date
//return question.getType() === 'text' && question.inputType === "date";
},
//Use this function to create a new class or add new properties or remove unneeded properties from your widget
//activatedBy tells how your widget has been activated by: property, type or customType
//property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date"
//type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons
//customType - you are creating a new type, like in our example "maxdiffmatrix"
activatedByChanged: function (activatedBy) {
//we do not need to check acticatedBy parameter, since we will use our widget for customType only
//We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us
Survey.JsonObject.metaData.addClass("maxdiffmatrix", [], null, "text");
//signaturepad is derived from "empty" class - basic question class
//Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty");
//Add new property(s)
//For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs
Survey.JsonObject.metaData.addProperties("maxdiffmatrix", [
{
name: "rows",
default: []
},
{
name: "columns",
default: []
}
]);
},
//If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate
isDefaultRender: false,
//You should use it if your set the isDefaultRender to false
htmlTemplate: "<div></div>",
//The main function, rendering and two-way binding
afterRender: function (question, el) {
console.log("MaxDiff mat", question.rows, question.columns);
new MaxDiffMatrix({ question, el });
// let containers = el.querySelectorAll(".srv-slider-container");
// let inputDOMS = el.querySelectorAll(".srv-slider");
// let sliderDisplayDOMS = el.querySelectorAll(".srv-slider-display");
// if (!(question.value instanceof Array))
// {
// question.value = new Array(inputDOMS.length);
// question.value.fill(0);
// }
// for (i = 0; i < inputDOMS.length; i++)
// {
// inputDOMS[i].min = question.minVal;
// inputDOMS[i].max = question.maxVal;
// inputDOMS[i].addEventListener("input", (e) => {
// let idx = parseInt(e.currentTarget.dataset.idx, 10);
// question.value[idx] = parseFloat(e.currentTarget.value);
// // using .value setter to trigger update properly.
// // otherwise on survey competion it returns array of nulls.
// question.value = question.value;
// onValueChangedCallback();
// });
// // Handle grid lines?
// }
// function positionSliderDisplay (v, min, max, displayDOM)
// {
// v = parseFloat(v);
// min = parseFloat(min);
// max = parseFloat(max);
// // Formula is (halfThumbWidth - v * (fullThumbWidth / 100)), taking into account that display has translate(-50%, 0).
// // Size of thumb is set in CSS.
// displayDOM.style.left = `calc(${(v - min) / (max - min) * 100}% + ${10 - v * 0.2}px)`
// }
// var onValueChangedCallback = function () {
// let i;
// let v;
// for (i = 0; i < question.choices.length; i++)
// {
// v = question.value[i] || 0;
// inputDOMS[i].value = v;
// sliderDisplayDOMS[i].innerText = v;
// positionSliderDisplay(v, question.minVal, question.maxVal, sliderDisplayDOMS[i]);
// }
// }
// var onReadOnlyChangedCallback = function() {
// let i;
// if (question.isReadOnly) {
// for (i = 0; i < question.choices.length; i++)
// {
// inputDOMS[i].setAttribute('disabled', 'disabled');
// }
// } else {
// for (i = 0; i < question.choices.length; i++)
// {
// inputDOMS[i].removeAttribute("disabled");
// }
// }
// };
// if question becomes readonly/enabled add/remove disabled attribute
// question.readOnlyChangedCallback = onReadOnlyChangedCallback;
// if the question value changed in the code, for example you have changed it in JavaScript
// question.valueChangedCallback = onValueChangedCallback;
// set initial value
// onValueChangedCallback();
// make elements disabled if needed
// onReadOnlyChangedCallback();
},
//Use it to destroy the widget. It is typically needed by jQuery widgets
willUnmount: function (question, el) {
//We do not need to clear anything in our simple example
//Here is the example to destroy the image picker
//var $el = $(el).find("select");
//$el.data('picker').destroy();
}
}
//Register our widget in singleton custom widget collection
Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype");
}

View File

@ -1,119 +0,0 @@
/**
* @desc SelectBox widget for surveyJS.
* @type: SurveyJS widget.
*/
export default function init (Survey) {
var widget = {
//the widget name. It should be unique and written in lowcase.
name: "selectbox",
//the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder
title: "My custom widg",
//the name of the icon on the toolbox. We will leave it empty to use the standard one
iconName: "",
//If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded
widgetIsLoaded: function () {
//return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page
return true; //we do not require anything so we just return true.
},
//SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior
isFit: function (question) {
//we return true if the type of question is selectbox
return question.getType() === 'selectbox';
//the following code will activate the widget for a text question with inputType equals to date
//return question.getType() === 'text' && question.inputType === "date";
},
//Use this function to create a new class or add new properties or remove unneeded properties from your widget
//activatedBy tells how your widget has been activated by: property, type or customType
//property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date"
//type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons
//customType - you are creating a new type, like in our example "selectbox"
activatedByChanged: function (activatedBy) {
//we do not need to check acticatedBy parameter, since we will use our widget for customType only
//We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us
Survey.JsonObject.metaData.addClass("selectbox", [], null, "text");
//signaturepad is derived from "empty" class - basic question class
//Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty");
//Add new property(s)
//For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs
Survey.JsonObject.metaData.addProperties("selectbox", [
{
name: "choices",
default: []
}
]);
},
//If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate
isDefaultRender: false,
//You should use it if your set the isDefaultRender to false
htmlTemplate: `<div><select class="srv-select-multiple" multiple></select></div>`,
//The main function, rendering and two-way binding
afterRender: function (question, el) {
let optionsHTML = "";
let i;
for (i = 0; i < question.choices.length; i++)
{
optionsHTML += `<option value="${question.choices[i].value}">${question.choices[i].text}</option>`;
}
let selectDOM = el.querySelector("select");
selectDOM.innerHTML = optionsHTML;
selectDOM.addEventListener('input', (e) => {
let i;
let opts = new Array(e.currentTarget.selectedOptions.length);
for (i = 0; i < e.currentTarget.selectedOptions.length; i++)
{
opts[i] = e.currentTarget.selectedOptions[i].value;
}
question.value = opts;
});
// var onValueChangedCallback = function () {
// text.value = question.value ? question.value : "";
// }
// var onReadOnlyChangedCallback = function() {
// if (question.isReadOnly) {
// text.setAttribute('disabled', 'disabled');
// button.setAttribute('disabled', 'disabled');
// } else {
// text.removeAttribute("disabled");
// button.removeAttribute("disabled");
// }
// };
//if question becomes readonly/enabled add/remove disabled attribute
// question.readOnlyChangedCallback = onReadOnlyChangedCallback;
//if the question value changed in the code, for example you have changed it in JavaScript
// question.valueChangedCallback = onValueChangedCallback;
//set initial value
// onValueChangedCallback();
//make elements disabled if needed
// onReadOnlyChangedCallback();
},
//Use it to destroy the widget. It is typically needed by jQuery widgets
willUnmount: function (question, el) {
//We do not need to clear anything in our simple example
//Here is the example to destroy the image picker
//var $el = $(el).find("select");
//$el.data('picker').destroy();
}
}
//Register our widget in singleton custom widget collection
Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype");
}

View File

@ -1,424 +0,0 @@
/**
* @desc Side By Side matrix.
* */
const CELL_TYPES = {
DROP_DOWN: "dropdown",
RADIO: "radio",
CHECKBOX: "checkbox",
TEXT: "text"
};
class SideBySideMatrix
{
constructor (cfg = {})
{
// surveyCSS contains css class names provided by the applied theme
// INCLUDING those added/modified by application's code.
const surveyCSS = cfg.question.css;
this._CSS_CLASSES = {
WRAPPER: surveyCSS.matrix.tableWrapper,
TABLE: surveyCSS.matrix.root,
TABLE_ROW: surveyCSS.matrixdropdown.row,
TABLE_HEADER_CELL: surveyCSS.matrix.headerCell,
TABLE_CELL: surveyCSS.matrix.cell,
INPUT_TEXT: surveyCSS.text.root,
LABEL: surveyCSS.matrix.label,
ITEM_CHECKED: surveyCSS.matrix.itemChecked,
ITEM_VALUE: surveyCSS.matrix.itemValue,
ITEM_DECORATOR: surveyCSS.matrix.materialDecorator,
RADIO: surveyCSS.radiogroup.item,
SELECT: surveyCSS.dropdown.control,
CHECKBOX: surveyCSS.checkbox.item,
CHECKBOX_CONTROL: surveyCSS.checkbox.itemControl,
CHECKBOX_DECORATOR: surveyCSS.checkbox.materialDecorator,
CHECKBOX_DECORATOR_SVG: surveyCSS.checkbox.itemDecorator
};
this._question = cfg.question;
this._DOM = cfg.el;
this._DOM.classList.add(...this._CSS_CLASSES.WRAPPER.split(" "));
this._bindedHandlers = {
_handleInput: this._handleInput.bind(this),
_handleSelectChange: this._handleSelectChange.bind(this)
};
this._init(this._question, this._DOM);
}
static CELL_GENERATORS =
{
[CELL_TYPES.DROP_DOWN]: "_generateDropdownCells",
[CELL_TYPES.RADIO]: "_generateRadioCells",
[CELL_TYPES.CHECKBOX]: "_generateCheckboxCells",
[CELL_TYPES.TEXT]: "_generateTextInputCells",
};
_generateDropdownCells (row, col, subColumns, CSS_CLASSES)
{
let bodyCells = "";
let selectOptions = "<option value=\"\"></option>";
let i;
for (i = 0; i < subColumns.length; i++)
{
selectOptions += `<option value="${subColumns[i].value}">${subColumns[i].text}</option>`;
}
bodyCells =
`<td class="${CSS_CLASSES.TABLE_CELL}">
<select class="${CSS_CLASSES.SELECT}" name="${row.value}-${col.value}">${selectOptions}</select>
</td>`;
return bodyCells;
}
_generateRadioCells (row, col, subColumns, CSS_CLASSES)
{
let bodyCells = "";
let i;
for (i = 0; i < subColumns.length; i++)
{
bodyCells +=
`<td class="${CSS_CLASSES.TABLE_CELL}">
<label class="${CSS_CLASSES.LABEL}">
<input class="${CSS_CLASSES.ITEM_VALUE}" type="${col.cellType}" name="${row.value}-${col.value}" value="${subColumns[i].value}">
<span class="${CSS_CLASSES.ITEM_DECORATOR}"></span>
</label>
</td>`;
}
return bodyCells;
}
_generateCheckboxCells (row, col, subColumns, CSS_CLASSES)
{
let bodyCells = "";
let i;
for (i = 0; i < subColumns.length; i++)
{
bodyCells +=
`<td class="${CSS_CLASSES.TABLE_CELL}">
<label class="${CSS_CLASSES.LABEL}">
<input class="${CSS_CLASSES.CHECKBOX_CONTROL}" type="${col.cellType}" name="${row.value}-${col.value}-${subColumns[i].value}">
<span class="${CSS_CLASSES.CHECKBOX_DECORATOR}">
<svg class="${CSS_CLASSES.CHECKBOX_DECORATOR_SVG}">
<use data-bind="attr:{'xlink:href':question.itemSvgIcon}" xlink:href="#icon-v2check"></use>
</svg>
</span>
</label>
</td>`;
}
return bodyCells;
}
_generateTextInputCells (row, col, subColumns, CSS_CLASSES)
{
let bodyCells = "";
let i;
for (i = 0; i < subColumns.length; i++)
{
bodyCells +=
`<td class="${CSS_CLASSES.TABLE_CELL}">
<input class="${CSS_CLASSES.INPUT_TEXT}" type="${col.cellType}" name="${row.value}-${col.value}-${subColumns[i].value}">
</td>`;
}
return bodyCells;
}
_ensureQuestionValueFields (row, col)
{
if (this._question.value === undefined)
{
this._question.value = {};
}
if (this._question.value[row] === undefined)
{
this._question.value[row] = {
[col]: {}
}
}
if (this._question.value[row][col] === undefined)
{
this._question.value[row][col] = {};
}
}
_handleInput (e)
{
const valueCoordinates = e.currentTarget.name.split("-");
const row = valueCoordinates[0];
const col = valueCoordinates[1];
const subCol = valueCoordinates[2] !== undefined ? valueCoordinates[2] : e.currentTarget.value;
this._ensureQuestionValueFields(row, col);
if (e.currentTarget.type === "text")
{
this._question.value[row][col][subCol] = e.currentTarget.value;
}
else if (e.currentTarget.type === "radio")
{
this._question.value[row][col] = e.currentTarget.value;
}
else if (e.currentTarget.type === "checkbox")
{
this._question.value[row][col][subCol] = e.currentTarget.checked;
}
// Triggering internal SurveyJS mechanism for value update.
this._question.value = this._question.value;
}
_handleSelectChange (e)
{
const valueCoordinates = e.currentTarget.name.split("-");
const row = valueCoordinates[0];
const col = valueCoordinates[1];
this._ensureQuestionValueFields(row, col);
this._question.value[row][col]= e.currentTarget.value;
// Triggering internal SurveyJS mechanism for value update.
this._question.value = this._question.value;
}
_init (question, el)
{
let t = performance.now();
const CSS_CLASSES = this._CSS_CLASSES;
// TODO: Find out how it actually composed inside SurveyJS.
if (question.css.matrix.mainRoot)
{
// Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior
const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`;
question.setCssRoot(rootClass);
question.cssClasses.mainRoot = rootClass;
}
let html;
let headerCells = "";
let subHeaderCells = "";
let bodyCells = "";
let bodyHTML = "";
let cellGenerator;
let i, j;
// Header generation
for (i = 0; i < question.columns.length; i++)
{
if (question.columns[i].cellType !== CELL_TYPES.DROP_DOWN)
{
headerCells +=
`<th class="${CSS_CLASSES.TABLE_HEADER_CELL}" colspan="${question.columns[i].subColumns.length}">
${question.columns[i].title}
</th>`;
for (j = 0; j < question.columns[i].subColumns.length; j++)
{
subHeaderCells += `<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[i].subColumns[j].text}</th>`;
}
}
else
{
headerCells +=
`<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">
${question.columns[i].title}
</th>`;
subHeaderCells += "<td></td>";
}
headerCells += "<td></td>";
subHeaderCells += "<td></td>";
}
// Body generation
for (i = 0; i < question.rows.length; i++)
{
bodyCells = "";
for (j = 0; j < question.columns.length; j++)
{
cellGenerator = this[SideBySideMatrix.CELL_GENERATORS[question.columns[j].cellType]];
if (typeof cellGenerator === "function")
{
// Passing rows, columns, subColumns as separate arguments
// to make generatorrs independent from table data-structure.
bodyCells += `${cellGenerator.call(this, question.rows[i], question.columns[j], question.columns[j].subColumns, CSS_CLASSES)}<td></td>`;
}
else
{
console.log("No cell generator found for cellType", question.columns[j].cellType);
}
}
bodyHTML += `<tr class="${CSS_CLASSES.TABLE_ROW}"><td class="${CSS_CLASSES.TABLE_CELL}">${question.rows[i].text}</td><td></td>${bodyCells}</tr>`;
}
html = `<table class="${CSS_CLASSES.TABLE}">
<thead>
<tr><th class="${CSS_CLASSES.TABLE_HEADER_CELL}"></th><td></td>${headerCells}</tr>
<tr><th class="${CSS_CLASSES.TABLE_HEADER_CELL}"></th><td></td>${subHeaderCells}</tr>
</thead>
<tbody>${bodyHTML}</tbody>
</table>`;
// console.log("sbs matrix generation took", performance.now() - t);
el.insertAdjacentHTML("beforeend", html);
let inputDOMS = el.querySelectorAll("input");
let selectDOMS = el.querySelectorAll("select");
for (i = 0; i < inputDOMS.length; i++)
{
inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput);
}
for (i = 0; i < selectDOMS.length; i++)
{
selectDOMS[i].addEventListener("change", this._bindedHandlers._handleSelectChange)
}
}
}
export default function init (Survey) {
var widget = {
//the widget name. It should be unique and written in lowcase.
name: "sidebysidematrix",
//the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder
title: "Side by side matrix",
//the name of the icon on the toolbox. We will leave it empty to use the standard one
iconName: "",
//If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded
widgetIsLoaded: function () {
//return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page
return true; //we do not require anything so we just return true.
},
//SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior
isFit: function (question) {
//we return true if the type of question is sidebysidematrix
return question.getType() === 'sidebysidematrix';
//the following code will activate the widget for a text question with inputType equals to date
//return question.getType() === 'text' && question.inputType === "date";
},
//Use this function to create a new class or add new properties or remove unneeded properties from your widget
//activatedBy tells how your widget has been activated by: property, type or customType
//property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date"
//type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons
//customType - you are creating a new type, like in our example "sidebysidematrix"
activatedByChanged: function (activatedBy) {
//we do not need to check acticatedBy parameter, since we will use our widget for customType only
//We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us
Survey.JsonObject.metaData.addClass("sidebysidematrix", [], null, "text");
//signaturepad is derived from "empty" class - basic question class
//Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty");
//Add new property(s)
//For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs
Survey.JsonObject.metaData.addProperties("sidebysidematrix", [
{
name: "rows",
default: []
},
{
name: "columns",
default: []
}
]);
},
//If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate
isDefaultRender: false,
//You should use it if your set the isDefaultRender to false
htmlTemplate: "<div></div>",
//The main function, rendering and two-way binding
afterRender: function (question, el) {
new SideBySideMatrix({ question, el });
// TODO: add readonly and enabled/disabled handlers.
// let containers = el.querySelectorAll(".srv-slider-container");
// let inputDOMS = el.querySelectorAll(".srv-slider");
// let sliderDisplayDOMS = el.querySelectorAll(".srv-slider-display");
// if (!(question.value instanceof Array))
// {
// question.value = new Array(inputDOMS.length);
// question.value.fill(0);
// }
// for (i = 0; i < inputDOMS.length; i++)
// {
// inputDOMS[i].min = question.minVal;
// inputDOMS[i].max = question.maxVal;
// inputDOMS[i].addEventListener("input", (e) => {
// let idx = parseInt(e.currentTarget.dataset.idx, 10);
// question.value[idx] = parseFloat(e.currentTarget.value);
// // using .value setter to trigger update properly.
// // otherwise on survey competion it returns array of nulls.
// question.value = question.value;
// onValueChangedCallback();
// });
// // Handle grid lines?
// }
// function positionSliderDisplay (v, min, max, displayDOM)
// {
// v = parseFloat(v);
// min = parseFloat(min);
// max = parseFloat(max);
// // Formula is (halfThumbWidth - v * (fullThumbWidth / 100)), taking into account that display has translate(-50%, 0).
// // Size of thumb is set in CSS.
// displayDOM.style.left = `calc(${(v - min) / (max - min) * 100}% + ${10 - v * 0.2}px)`
// }
// var onValueChangedCallback = function () {
// let i;
// let v;
// for (i = 0; i < question.choices.length; i++)
// {
// v = question.value[i] || 0;
// inputDOMS[i].value = v;
// sliderDisplayDOMS[i].innerText = v;
// positionSliderDisplay(v, question.minVal, question.maxVal, sliderDisplayDOMS[i]);
// }
// }
// var onReadOnlyChangedCallback = function() {
// let i;
// if (question.isReadOnly) {
// for (i = 0; i < question.choices.length; i++)
// {
// inputDOMS[i].setAttribute('disabled', 'disabled');
// }
// } else {
// for (i = 0; i < question.choices.length; i++)
// {
// inputDOMS[i].removeAttribute("disabled");
// }
// }
// };
// if question becomes readonly/enabled add/remove disabled attribute
// question.readOnlyChangedCallback = onReadOnlyChangedCallback;
// if the question value changed in the code, for example you have changed it in JavaScript
// question.valueChangedCallback = onValueChangedCallback;
// set initial value
// onValueChangedCallback();
// make elements disabled if needed
// onReadOnlyChangedCallback();
},
//Use it to destroy the widget. It is typically needed by jQuery widgets
willUnmount: function (question, el) {
//We do not need to clear anything in our simple example
//Here is the example to destroy the image picker
//var $el = $(el).find("select");
//$el.data('picker').destroy();
}
}
//Register our widget in singleton custom widget collection
Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype");
}

View File

@ -1,289 +0,0 @@
/**
* @desc Slider Star.
* */
class SliderStar
{
constructor (cfg = {})
{
const surveyCSS = cfg.question.css;
this._CSS_CLASSES = {
// INPUT_TEXT: `${surveyCSS.text.root} slider-star-text-input`
INPUT_TEXT: `slider-star-text-input`
};
this._question = cfg.question;
this._DOM = cfg.el;
this._engagedInputIdx = undefined;
this._pdowns = {};
this._bindedHandlers =
{
_handleInput: this._handleInput.bind(this),
_handlePointerDown: this._handlePointerDown.bind(this),
_handlePointerUp: this._handlePointerUp.bind(this),
_handlePointerMove: this._handlePointerMove.bind(this)
};
this._init(this._question, this._DOM);
}
_markStarsActive (n, qIdx)
{
let stars = this._DOM.querySelectorAll(`.stars-container[data-idx="${qIdx}"] .star-slider-star-input`);
let i;
for (i = 0; i < stars.length; i++)
{
stars[i].classList.remove("active");
if (i <= n - 1)
{
stars[i].classList.add("active");
}
}
}
_handleIndividualValueUpdate (v, qIdx)
{
if (this._question.value === undefined)
{
this._question.value = {};
}
if (this._question.value[qIdx] !== v)
{
this._question.value[qIdx] = v;
this._DOM.querySelector(`.slider-star-text-input[name="${qIdx}"]`).value = v;
this._markStarsActive(v, qIdx);
// Triggering internal SurveyJS mechanism for value update.
this._question.value = this._question.value;
}
}
_handleInput (e)
{
let v = parseInt(e.currentTarget.value, 10) || 0;
v = Math.max(0, Math.min(this._question.starCount, v));
const qIdx = e.currentTarget.name;
this._handleIndividualValueUpdate(v, qIdx);
}
_handlePointerDown (e)
{
e.preventDefault();
this._engagedInputIdx = e.currentTarget.dataset.idx;
this._pdowns[this._engagedInputIdx] = true;
const starIdx = [].indexOf.call(e.target.parentElement.children, e.target);
this._handleIndividualValueUpdate(starIdx + 1, this._engagedInputIdx);
}
_handlePointerUp (e)
{
if (this._engagedInputIdx !== undefined)
{
this._pdowns[this._engagedInputIdx] = false;
}
this._engagedInputIdx = undefined;
}
_handlePointerMove (e)
{
if (this._pdowns[this._engagedInputIdx])
{
e.preventDefault();
const starIdx = [].indexOf.call(e.target.parentElement.children, e.target);
this._handleIndividualValueUpdate(starIdx + 1, this._engagedInputIdx);
}
}
_init (question, el)
{
let t = performance.now();
let starsHTML = new Array(question.starCount).fill(`<div class="star-slider-star-input">★</div>`).join("");
let html = "";
let i;
for (i = 0; i < question.choices.length; i++)
{
html +=
`<div class="star-slider-container">
<div class="star-slider-title">${question.choices[i].text}</div>
<div class="star-slider-inputs">
<div class="stars-container" data-idx="${question.choices[i].value}">${starsHTML}</div>
${question.showValue ?
`<input type="number" class="${this._CSS_CLASSES.INPUT_TEXT}" max="${question.starCount}" min="0" name="${question.choices[i].value}">` :
""}
</div>
</div>`;
}
el.insertAdjacentHTML("beforeend", html);
const inputDOMS = el.querySelectorAll(".slider-star-text-input");
const starsContainers = el.querySelectorAll(".stars-container");
// Amount of inputDOMS and starsCointainer is the same.
// Also iterating over starContainers since text inputs might be absent.
for (i = 0; i < starsContainers.length; i++)
{
inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput);
starsContainers[i].addEventListener("pointerdown", this._bindedHandlers._handlePointerDown);
starsContainers[i].addEventListener("pointermove", this._bindedHandlers._handlePointerMove);
}
window.addEventListener("pointerup", this._bindedHandlers._handlePointerUp);
}
}
export default function init (Survey) {
var widget = {
//the widget name. It should be unique and written in lowcase.
name: "sliderstar",
//the widget title. It is how it will appear on the toolbox of the SurveyJS Editor/Builder
title: "Slider Star",
//the name of the icon on the toolbox. We will leave it empty to use the standard one
iconName: "",
//If the widgets depends on third-party library(s) then here you may check if this library(s) is loaded
widgetIsLoaded: function () {
//return typeof $ == "function" && !!$.fn.select2; //return true if jQuery and select2 widget are loaded on the page
return true; //we do not require anything so we just return true.
},
//SurveyJS library calls this function for every question to check, if it should use this widget instead of default rendering/behavior
isFit: function (question) {
//we return true if the type of question is sliderstar
return question.getType() === 'sliderstar';
//the following code will activate the widget for a text question with inputType equals to date
//return question.getType() === 'text' && question.inputType === "date";
},
//Use this function to create a new class or add new properties or remove unneeded properties from your widget
//activatedBy tells how your widget has been activated by: property, type or customType
//property - it means that it will activated if a property of the existing question type is set to particular value, for example inputType = "date"
//type - you are changing the behaviour of entire question type. For example render radiogroup question differently, have a fancy radio buttons
//customType - you are creating a new type, like in our example "sliderstar"
activatedByChanged: function (activatedBy) {
//we do not need to check acticatedBy parameter, since we will use our widget for customType only
//We are creating a new class and derived it from text question type. It means that text model (properties and fuctions) will be available to us
Survey.JsonObject.metaData.addClass("sliderstar", [], null, "text");
//signaturepad is derived from "empty" class - basic question class
//Survey.JsonObject.metaData.addClass("signaturepad", [], null, "empty");
//Add new property(s)
//For more information go to https://surveyjs.io/Examples/Builder/?id=addproperties#content-docs
Survey.JsonObject.metaData.addProperties("sliderstar", [
{
name: "choices",
default: []
},
{
name: "starCount",
default: 5
},
{
name: "showValue",
default: true
},
{
name: "starType",
default: "descrete"
}
]);
},
//If you want to use the default question rendering then set this property to true. We do not need any default rendering, we will use our our htmlTemplate
isDefaultRender: false,
//You should use it if your set the isDefaultRender to false
htmlTemplate: "<div></div>",
//The main function, rendering and two-way binding
afterRender: function (question, el) {
new SliderStar({ question, el });
// let containers = el.querySelectorAll(".srv-slider-container");
// let inputDOMS = el.querySelectorAll(".srv-slider");
// let sliderDisplayDOMS = el.querySelectorAll(".srv-slider-display");
// if (!(question.value instanceof Array))
// {
// question.value = new Array(inputDOMS.length);
// question.value.fill(0);
// }
// for (i = 0; i < inputDOMS.length; i++)
// {
// inputDOMS[i].min = question.minVal;
// inputDOMS[i].max = question.maxVal;
// inputDOMS[i].addEventListener("input", (e) => {
// let idx = parseInt(e.currentTarget.dataset.idx, 10);
// question.value[idx] = parseFloat(e.currentTarget.value);
// // using .value setter to trigger update properly.
// // otherwise on survey competion it returns array of nulls.
// question.value = question.value;
// onValueChangedCallback();
// });
// // Handle grid lines?
// }
// function positionSliderDisplay (v, min, max, displayDOM)
// {
// v = parseFloat(v);
// min = parseFloat(min);
// max = parseFloat(max);
// // Formula is (halfThumbWidth - v * (fullThumbWidth / 100)), taking into account that display has translate(-50%, 0).
// // Size of thumb is set in CSS.
// displayDOM.style.left = `calc(${(v - min) / (max - min) * 100}% + ${10 - v * 0.2}px)`
// }
// var onValueChangedCallback = function () {
// let i;
// let v;
// for (i = 0; i < question.choices.length; i++)
// {
// v = question.value[i] || 0;
// inputDOMS[i].value = v;
// sliderDisplayDOMS[i].innerText = v;
// positionSliderDisplay(v, question.minVal, question.maxVal, sliderDisplayDOMS[i]);
// }
// }
// var onReadOnlyChangedCallback = function() {
// let i;
// if (question.isReadOnly) {
// for (i = 0; i < question.choices.length; i++)
// {
// inputDOMS[i].setAttribute('disabled', 'disabled');
// }
// } else {
// for (i = 0; i < question.choices.length; i++)
// {
// inputDOMS[i].removeAttribute("disabled");
// }
// }
// };
// if question becomes readonly/enabled add/remove disabled attribute
// question.readOnlyChangedCallback = onReadOnlyChangedCallback;
// if the question value changed in the code, for example you have changed it in JavaScript
// question.valueChangedCallback = onValueChangedCallback;
// set initial value
// onValueChangedCallback();
// make elements disabled if needed
// onReadOnlyChangedCallback();
},
//Use it to destroy the widget. It is typically needed by jQuery widgets
willUnmount: function (question, el) {
//We do not need to clear anything in our simple example
//Here is the example to destroy the image picker
//var $el = $(el).find("select");
//$el.data('picker').destroy();
}
}
//Register our widget in singleton custom widget collection
Survey.CustomWidgetCollection.Instance.addCustomWidget(widget, "customtype");
}

View File

@ -0,0 +1,48 @@
/**
* @desc: Extensions for default dropdown component of SurveyJS to make it more nice to interact with on mobile devices.
* @type: SurveyJS component modification.
*/
function handleValueChange (survey, options, e)
{
options.question.value = e.currentTarget.value;
}
function handleValueChangeForDOM (survey, options)
{
options.htmlElement.querySelector("select").value = options.question.value;
}
function handleDropdownRendering (survey, options)
{
// Default SurveyJS drop down is actually an <input> with customly built options list
// It works well on desktop, but not that convenient on mobile.
// Adding native <select> here that's hidden by default but visible on mobile.
const surveyCSS = options.question.css;
const choices = options.question.getChoices();
let optionsHTML = `<option value=""></option>`;
let i;
for (i = 0; i < choices.length; i++)
{
optionsHTML += `<option value="${choices[i].value}">${choices[i].text}</option>`;
}
const selectHTML = `<select data-name="${options.question.name}" class="${surveyCSS.dropdown.control} dropdown-mobile">${optionsHTML}</select>`;
options.htmlElement.querySelector('.sd-selectbase').insertAdjacentHTML("beforebegin", selectHTML);
const selectDOM = options.htmlElement.querySelector("select");
selectDOM.addEventListener("change", handleValueChange.bind(this, survey, options));
options.question.valueChangedCallback = handleValueChangeForDOM.bind(this, survey, options);
}
export default {
registerModelCallbacks (surveyModel)
{
surveyModel.onAfterRenderQuestion.add((survey, options) => {
if (options.question.getType() === "dropdown")
{
handleDropdownRendering(survey, options);
}
});
}
};

View File

@ -22,7 +22,8 @@ function handleBipolarMatrixRendering (survey, options)
let rowsDOM = options.htmlElement.querySelectorAll("tbody tr"); let rowsDOM = options.htmlElement.querySelectorAll("tbody tr");
// let rowCaptionsDOM = options.htmlElement.querySelectorAll("tbody tr td:nth-child(1) .sv-string-viewer"); // let rowCaptionsDOM = options.htmlElement.querySelectorAll("tbody tr td:nth-child(1) .sv-string-viewer");
let rowCaptionsDOM = options.htmlElement.querySelectorAll("tbody tr td:nth-child(1) span"); let rowCaptionsDOM = options.htmlElement.querySelectorAll("tbody tr td:nth-child(1) span");
let captionsClassList = rowCaptionsDOM[0].classList.toString(); let captionsClassList = rowCaptionsDOM[0].classList;
let cellClassList = rowsDOM[0].children[0].classList;
let rowCaptions = new Array(options.question.rows.length); let rowCaptions = new Array(options.question.rows.length);
let rowCaptionOppositeHTML = ""; let rowCaptionOppositeHTML = "";
let i; let i;
@ -30,7 +31,7 @@ function handleBipolarMatrixRendering (survey, options)
{ {
rowCaptions[i] = options.question.rows[i].text.split(":"); rowCaptions[i] = options.question.rows[i].text.split(":");
rowCaptionsDOM[i].innerText = rowCaptions[i][0]; rowCaptionsDOM[i].innerText = rowCaptions[i][0];
rowCaptionOppositeHTML = `<td><span class="${captionsClassList}">${rowCaptions[i][1]}</span></td>`; rowCaptionOppositeHTML = `<td class="${cellClassList.value}"><span class="${captionsClassList.value}">${rowCaptions[i][1]}</span></td>`;
rowsDOM[i].insertAdjacentHTML("beforeend", rowCaptionOppositeHTML); rowsDOM[i].insertAdjacentHTML("beforeend", rowCaptionOppositeHTML);
} }
} }
@ -38,7 +39,7 @@ function handleBipolarMatrixRendering (survey, options)
export default { export default {
registerSurveyProperties (Survey) registerSurveyProperties (Survey)
{ {
Survey.Serializer.addProperty("question", Survey.Serializer.addProperty("matrix",
{ {
name: "subType:text", name: "subType:text",
default: "", default: "",

View File

@ -0,0 +1,89 @@
// Wrapping everything in Class and defining as static methods to prevent esbuild from renaming when bundling.
// NOTE! Survey stim uses property .name of these methods on registering stage.
// Methods are available inside SurveyJS expressions using their actual names.
class ExpressionFunctions {
static rnd ()
{
return Math.random();
}
static arrayContains (params)
{
if (params[0] instanceof Array)
{
let searchValue = params[1];
let searchResult = params[0].indexOf(searchValue) !== -1;
// If no results at first, trying conversion combinations, since array of values sometimes might
// contain both string and number data types.
if (searchResult === false)
{
if (typeof searchValue === "string")
{
searchValue = parseFloat(searchValue);
searchResult = params[0].indexOf(searchValue) !== -1;
}
else if (typeof searchValue === "number")
{
searchValue = searchValue.toString();
searchResult = params[0].indexOf(searchValue) !== -1;
}
}
return searchResult
}
return false;
}
static stringContains (params)
{
if (typeof params[0] === "string")
{
return params[0].indexOf(params[1]) !== -1;
}
return false;
}
static isEmpty (params)
{
let questionIsEmpty = false;
if (params[0] instanceof Array || typeof params[0] === "string")
{
questionIsEmpty = params[0].length === 0;
}
else
{
questionIsEmpty = params[0] === undefined || params[0] === null;
}
return questionIsEmpty;
}
static isNotEmpty (params)
{
return !ExpressionFunctions.isEmpty(params);
}
}
export default [
{
func: ExpressionFunctions.rnd,
isAsync: false
},
{
func: ExpressionFunctions.arrayContains,
isAsync: false
},
{
func: ExpressionFunctions.stringContains,
isAsync: false
},
{
func: ExpressionFunctions.isEmpty,
isAsync: false
},
{
func: ExpressionFunctions.isNotEmpty,
isAsync: false
}
];

View File

@ -16,6 +16,10 @@ class MaxDiffMatrix
TABLE_HEADER_CELL: surveyCSS.matrix.headerCell, TABLE_HEADER_CELL: surveyCSS.matrix.headerCell,
TABLE_CELL: surveyCSS.matrix.cell, TABLE_CELL: surveyCSS.matrix.cell,
INPUT_TEXT: surveyCSS.text.root, INPUT_TEXT: surveyCSS.text.root,
LABEL: surveyCSS.matrix.label,
ITEM_CHECKED: surveyCSS.matrix.itemChecked,
ITEM_VALUE: surveyCSS.matrix.itemValue,
ITEM_DECORATOR: surveyCSS.matrix.materialDecorator,
RADIO: surveyCSS.radiogroup.item, RADIO: surveyCSS.radiogroup.item,
SELECT: surveyCSS.dropdown.control, SELECT: surveyCSS.dropdown.control,
CHECKBOX: surveyCSS.checkbox.item CHECKBOX: surveyCSS.checkbox.item
@ -84,18 +88,18 @@ class MaxDiffMatrix
{ {
let t = performance.now(); let t = performance.now();
const CSS_CLASSES = this._CSS_CLASSES; const CSS_CLASSES = this._CSS_CLASSES;
let html; if (question.css.matrix.mainRoot)
let headerCells = ""; {
let subHeaderCells = ""; // Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior
let bodyCells = ""; const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`;
let bodyHTML = ""; question.setCssRoot(rootClass);
let cellGenerator; question.cssClasses.mainRoot = rootClass;
let i, j; }
// Relying on a fact that there's always 2 columns. // Relying on a fact that there's always 2 columns.
// This is correct according current Qualtrics design for MaxDiff matrices. // This is correct according current Qualtrics design for MaxDiff matrices.
// Header generation // Header generation
headerCells = let headerCells =
`<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[0].text}</th> `<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[0].text}</th>
<td></td> <td></td>
<td></td> <td></td>
@ -103,18 +107,29 @@ class MaxDiffMatrix
<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[1].text}</th>`; <th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[1].text}</th>`;
// Body generation // 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}"><input type="radio" class="${CSS_CLASSES.RADIO}" name="${question.rows[i].value}" data-column=${question.columns[0].value}></td> `<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}>
<span class="${CSS_CLASSES.ITEM_DECORATOR}"></span>
</label>
</td>
<td></td> <td></td>
<td class="${CSS_CLASSES.TABLE_CELL}">${question.rows[i].text}</td> <td class="${CSS_CLASSES.TABLE_CELL}">${question.rows[i].text}</td>
<td></td> <td></td>
<td class="${CSS_CLASSES.TABLE_CELL}"><input type="radio" class="${CSS_CLASSES.RADIO}" name="${question.rows[i].value}" data-column=${question.columns[1].value}></td>`; <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[1].value}>
<span class="${CSS_CLASSES.ITEM_DECORATOR}"></span>
</label>
</td>`;
bodyHTML += `<tr class="${CSS_CLASSES.TABLE_ROW}">${bodyCells}</tr>`; bodyHTML += `<tr class="${CSS_CLASSES.TABLE_ROW}">${bodyCells}</tr>`;
} }
html = `<table class="${CSS_CLASSES.TABLE}"> let html = `<table class="${CSS_CLASSES.TABLE}">
<thead> <thead>
<tr>${headerCells}</tr> <tr>${headerCells}</tr>
</thead> </thead>
@ -126,14 +141,15 @@ class MaxDiffMatrix
let inputDOMS = el.querySelectorAll("input"); 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); inputDOMS[i].addEventListener("input", this._bindedHandlers._handleInput);
} }
} }
} }
export default function init (Survey) { export default function init (Survey)
{
var widget = { var widget = {
//the widget name. It should be unique and written in lowcase. //the widget name. It should be unique and written in lowcase.
name: "maxdiffmatrix", name: "maxdiffmatrix",
@ -175,10 +191,12 @@ export default function init (Survey) {
Survey.JsonObject.metaData.addProperties("maxdiffmatrix", [ Survey.JsonObject.metaData.addProperties("maxdiffmatrix", [
{ {
name: "rows", name: "rows",
isArray: true,
default: [] default: []
}, },
{ {
name: "columns", name: "columns",
isArray: true,
default: [] default: []
} }
]); ]);

View File

@ -45,7 +45,12 @@ export default function init (Survey) {
Survey.JsonObject.metaData.addProperties("selectbox", [ Survey.JsonObject.metaData.addProperties("selectbox", [
{ {
name: "choices", name: "choices",
isArray: true,
default: [] default: []
},
{
name: "multipleAnswer",
default: true
} }
]); ]);
}, },
@ -54,7 +59,7 @@ export default function init (Survey) {
isDefaultRender: false, isDefaultRender: false,
//You should use it if your set the isDefaultRender to false //You should use it if your set the isDefaultRender to false
htmlTemplate: "<div><select multiple></select></div>", htmlTemplate: `<div></div>`,
//The main function, rendering and two-way binding //The main function, rendering and two-way binding
afterRender: function (question, el) { afterRender: function (question, el) {
@ -65,9 +70,20 @@ export default function init (Survey) {
optionsHTML += `<option value="${question.choices[i].value}">${question.choices[i].text}</option>`; optionsHTML += `<option value="${question.choices[i].value}">${question.choices[i].text}</option>`;
} }
let selectDOM = el.querySelector("select"); let additionalAttr = "";
selectDOM.innerHTML = optionsHTML; if (question.multipleAnswer)
{
additionalAttr = "multiple";
}
else
{
additionalAttr = "size=\"4\"";
}
let selectHTML = `<select class="srv-select-multiple" ${additionalAttr}>${optionsHTML}</select>`;
el.insertAdjacentHTML("beforeend", selectHTML);
let selectDOM = el.querySelector("select");
selectDOM.addEventListener('input', (e) => { selectDOM.addEventListener('input', (e) => {
let i; let i;
let opts = new Array(e.currentTarget.selectedOptions.length); let opts = new Array(e.currentTarget.selectedOptions.length);

View File

@ -17,15 +17,22 @@ class SideBySideMatrix
// INCLUDING those added/modified by application's code. // INCLUDING those added/modified by application's code.
const surveyCSS = cfg.question.css; const surveyCSS = cfg.question.css;
this._CSS_CLASSES = { this._CSS_CLASSES = {
WRAPPER: surveyCSS.matrix.tableWrapper, WRAPPER: `${surveyCSS.matrix.tableWrapper} sbs-matrix`,
TABLE: surveyCSS.matrix.root, TABLE: surveyCSS.matrix.root,
TABLE_ROW: surveyCSS.matrixdropdown.row, TABLE_ROW: surveyCSS.matrixdropdown.row,
TABLE_HEADER_CELL: surveyCSS.matrix.headerCell, TABLE_HEADER_CELL: surveyCSS.matrix.headerCell,
TABLE_CELL: surveyCSS.matrix.cell, TABLE_CELL: surveyCSS.matrix.cell,
INPUT_TEXT: surveyCSS.text.root, INPUT_TEXT: surveyCSS.text.root,
LABEL: surveyCSS.matrix.label,
ITEM_CHECKED: surveyCSS.matrix.itemChecked,
ITEM_VALUE: surveyCSS.matrix.itemValue,
ITEM_DECORATOR: surveyCSS.matrix.materialDecorator,
RADIO: surveyCSS.radiogroup.item, RADIO: surveyCSS.radiogroup.item,
SELECT: surveyCSS.dropdown.control, SELECT: surveyCSS.dropdown.control,
CHECKBOX: surveyCSS.checkbox.item CHECKBOX: surveyCSS.checkbox.item,
CHECKBOX_CONTROL: surveyCSS.checkbox.itemControl,
CHECKBOX_DECORATOR: surveyCSS.checkbox.materialDecorator,
CHECKBOX_DECORATOR_SVG: surveyCSS.checkbox.itemDecorator
}; };
this._question = cfg.question; this._question = cfg.question;
this._DOM = cfg.el; this._DOM = cfg.el;
@ -71,7 +78,10 @@ class SideBySideMatrix
{ {
bodyCells += bodyCells +=
`<td class="${CSS_CLASSES.TABLE_CELL}"> `<td class="${CSS_CLASSES.TABLE_CELL}">
<input class="${CSS_CLASSES.RADIO}" type="${col.cellType}" name="${row.value}-${col.value}" value="${subColumns[i].value}"> <label class="${CSS_CLASSES.LABEL}">
<input class="${CSS_CLASSES.ITEM_VALUE}" type="${col.cellType}" name="${row.value}-${col.value}" value="${subColumns[i].value}">
<span class="${CSS_CLASSES.ITEM_DECORATOR}"></span>
</label>
</td>`; </td>`;
} }
return bodyCells; return bodyCells;
@ -85,7 +95,14 @@ class SideBySideMatrix
{ {
bodyCells += bodyCells +=
`<td class="${CSS_CLASSES.TABLE_CELL}"> `<td class="${CSS_CLASSES.TABLE_CELL}">
<input class="${CSS_CLASSES.CHECKBOX}" type="${col.cellType}" name="${row.value}-${col.value}-${subColumns[i].value}"> <label class="${CSS_CLASSES.LABEL}">
<input class="${CSS_CLASSES.CHECKBOX_CONTROL}" type="${col.cellType}" name="${row.value}-${col.value}-${subColumns[i].value}">
<span class="${CSS_CLASSES.CHECKBOX_DECORATOR}">
<svg class="${CSS_CLASSES.CHECKBOX_DECORATOR_SVG}">
<use data-bind="attr:{'xlink:href':question.itemSvgIcon}" xlink:href="#icon-v2check"></use>
</svg>
</span>
</label>
</td>`; </td>`;
} }
return bodyCells; return bodyCells;
@ -168,7 +185,10 @@ class SideBySideMatrix
// TODO: Find out how it actually composed inside SurveyJS. // TODO: Find out how it actually composed inside SurveyJS.
if (question.css.matrix.mainRoot) if (question.css.matrix.mainRoot)
{ {
question.setCssRoot(`${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`); // Replacing default mainRoot class with those used in matrix type questions, to achieve proper styling and overflow behavior
const rootClass = `${question.css.matrix.mainRoot} ${question.cssClasses.withFrame || ""}`;
question.setCssRoot(rootClass);
question.cssClasses.mainRoot = rootClass;
} }
let html; let html;
let headerCells = ""; let headerCells = "";
@ -189,7 +209,10 @@ class SideBySideMatrix
</th>`; </th>`;
for (j = 0; j < question.columns[i].subColumns.length; j++) for (j = 0; j < question.columns[i].subColumns.length; j++)
{ {
subHeaderCells += `<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">${question.columns[i].subColumns[j].text}</th>`; subHeaderCells += `<th
class="${CSS_CLASSES.TABLE_HEADER_CELL} sbs-matrix-header-cell--${question.columns[i].cellType}">
${question.columns[i].subColumns[j].text}
</th>`;
} }
} }
else else
@ -198,7 +221,7 @@ class SideBySideMatrix
`<th class="${CSS_CLASSES.TABLE_HEADER_CELL}"> `<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">
${question.columns[i].title} ${question.columns[i].title}
</th>`; </th>`;
subHeaderCells += "<td></td>"; subHeaderCells += `<td class="${CSS_CLASSES.TABLE_HEADER_CELL} sbs-matrix-header-cell--${question.columns[i].cellType}"></td>`;
} }
headerCells += "<td></td>"; headerCells += "<td></td>";
subHeaderCells += "<td></td>"; subHeaderCells += "<td></td>";
@ -227,8 +250,8 @@ class SideBySideMatrix
html = `<table class="${CSS_CLASSES.TABLE}"> html = `<table class="${CSS_CLASSES.TABLE}">
<thead> <thead>
<tr><th class="${CSS_CLASSES.TABLE_HEADER_CELL}"></th><td></td>${headerCells}</tr> <tr><td></td><td></td>${headerCells}</tr>
<tr><th class="${CSS_CLASSES.TABLE_HEADER_CELL}"></th><td></td>${subHeaderCells}</tr> <tr><td></td><td></td>${subHeaderCells}</tr>
</thead> </thead>
<tbody>${bodyHTML}</tbody> <tbody>${bodyHTML}</tbody>
</table>`; </table>`;
@ -293,10 +316,12 @@ export default function init (Survey) {
Survey.JsonObject.metaData.addProperties("sidebysidematrix", [ Survey.JsonObject.metaData.addProperties("sidebysidematrix", [
{ {
name: "rows", name: "rows",
isArray: true,
default: [] default: []
}, },
{ {
name: "columns", name: "columns",
isArray: true,
default: [] default: []
} }
]); ]);

View File

@ -6,6 +6,11 @@ class SliderStar
{ {
constructor (cfg = {}) constructor (cfg = {})
{ {
const surveyCSS = cfg.question.css;
this._CSS_CLASSES = {
// INPUT_TEXT: `${surveyCSS.text.root} slider-star-text-input`
INPUT_TEXT: `slider-star-text-input`
};
this._question = cfg.question; this._question = cfg.question;
this._DOM = cfg.el; this._DOM = cfg.el;
this._engagedInputIdx = undefined; this._engagedInputIdx = undefined;
@ -102,7 +107,7 @@ class SliderStar
<div class="star-slider-inputs"> <div class="star-slider-inputs">
<div class="stars-container" data-idx="${question.choices[i].value}">${starsHTML}</div> <div class="stars-container" data-idx="${question.choices[i].value}">${starsHTML}</div>
${question.showValue ? ${question.showValue ?
`<input type="number" class="slider-star-text-input" max="${question.starCount}" min="0" name="${question.choices[i].value}">` : `<input type="number" class="${this._CSS_CLASSES.INPUT_TEXT}" max="${question.starCount}" min="0" name="${question.choices[i].value}">` :
""} ""}
</div> </div>
</div>`; </div>`;
@ -166,6 +171,7 @@ export default function init (Survey) {
Survey.JsonObject.metaData.addProperties("sliderstar", [ Survey.JsonObject.metaData.addProperties("sliderstar", [
{ {
name: "choices", name: "choices",
isArray: true,
default: [] default: []
}, },
{ {

View File

@ -44,6 +44,7 @@ export default function init (Survey) {
Survey.JsonObject.metaData.addProperties("slider", [ Survey.JsonObject.metaData.addProperties("slider", [
{ {
name: "choices", name: "choices",
isArray: true,
default: [] default: []
}, },
{ {