mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 18:50:54 +00:00
Merge branch '2023.2.0' into progress-bar
This commit is contained in:
commit
756c4986fa
2
.github/workflows/Automated Test (full).yml
vendored
2
.github/workflows/Automated Test (full).yml
vendored
@ -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:
|
||||||
|
2
.github/workflows/Automated Test (short).yml
vendored
2
.github/workflows/Automated Test (short).yml
vendored
@ -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
1002
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
@ -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")
|
||||||
|
@ -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'>×</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'>×</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>";
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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" ||
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
@ -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)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
* @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip
|
* @param {boolean} [options.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
|
||||||
*/
|
*/
|
||||||
constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, autoDraw, autoLog } = {})
|
constructor({ name, win, image, mask, pos, anchor, units, ori, size, color, opacity, contrast, texRes, depth, interpolate, flipHoriz, flipVert, aspectRatio, autoDraw, autoLog } = {})
|
||||||
{
|
{
|
||||||
super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog });
|
super({ name, win, units, ori, opacity, depth, pos, anchor, size, autoDraw, autoLog });
|
||||||
|
|
||||||
@ -94,6 +94,12 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
false,
|
false,
|
||||||
this._onChange(false, false),
|
this._onChange(false, false),
|
||||||
);
|
);
|
||||||
|
this._addAttribute(
|
||||||
|
"aspectRatio",
|
||||||
|
aspectRatio,
|
||||||
|
ImageStim.AspectRatioStrategy.VARIABLE,
|
||||||
|
this._onChange(true, true),
|
||||||
|
);
|
||||||
|
|
||||||
// estimate the bounding box:
|
// estimate the bounding box:
|
||||||
this._estimateBoundingBox();
|
this._estimateBoundingBox();
|
||||||
@ -309,7 +315,18 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
this._texture = new PIXI.Texture(new PIXI.BaseTexture(this._image, texOpts));
|
this._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")
|
||||||
@ -349,8 +366,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;
|
||||||
@ -383,7 +416,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"),
|
||||||
|
};
|
||||||
|
@ -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
@ -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
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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");
|
|
||||||
}
|
|
@ -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");
|
|
||||||
}
|
|
@ -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");
|
|
||||||
}
|
|
@ -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");
|
|
||||||
}
|
|
48
src/visual/survey/components/DropdownExtensions.js
Normal file
48
src/visual/survey/components/DropdownExtensions.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -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: "",
|
||||||
|
89
src/visual/survey/extensions/customExpressionFunctions.js
Normal file
89
src/visual/survey/extensions/customExpressionFunctions.js
Normal 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
|
||||||
|
}
|
||||||
|
];
|
@ -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: []
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
@ -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);
|
||||||
|
@ -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: []
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
@ -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: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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: []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user