mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 10:40:54 +00:00
commit
ec3283bb7b
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "psychojs",
|
||||
"version": "2022.2.5",
|
||||
"version": "2022.3.1",
|
||||
"private": true,
|
||||
"description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments",
|
||||
"license": "MIT",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -65,6 +65,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
|
||||
opacity,
|
||||
depth,
|
||||
text,
|
||||
placeholder,
|
||||
font,
|
||||
letterHeight,
|
||||
bold,
|
||||
@ -98,7 +99,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
|
||||
);
|
||||
this._addAttribute(
|
||||
"placeholder",
|
||||
text,
|
||||
placeholder,
|
||||
"",
|
||||
this._onChange(true, true),
|
||||
);
|
||||
|
@ -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 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 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 rowCaptionOppositeHTML = "";
|
||||
let i;
|
||||
@ -30,7 +31,7 @@ function handleBipolarMatrixRendering (survey, options)
|
||||
{
|
||||
rowCaptions[i] = options.question.rows[i].text.split(":");
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -38,7 +39,7 @@ function handleBipolarMatrixRendering (survey, options)
|
||||
export default {
|
||||
registerSurveyProperties (Survey)
|
||||
{
|
||||
Survey.Serializer.addProperty("question",
|
||||
Survey.Serializer.addProperty("matrix",
|
||||
{
|
||||
name: "subType:text",
|
||||
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_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
|
||||
@ -84,6 +88,13 @@ class MaxDiffMatrix
|
||||
{
|
||||
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 = "";
|
||||
@ -106,11 +117,21 @@ class MaxDiffMatrix
|
||||
for (i = 0; i < question.rows.length; i++)
|
||||
{
|
||||
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 class="${CSS_CLASSES.TABLE_CELL}">${question.rows[i].text}</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>`;
|
||||
}
|
||||
|
||||
@ -175,10 +196,12 @@ export default function init (Survey) {
|
||||
Survey.JsonObject.metaData.addProperties("maxdiffmatrix", [
|
||||
{
|
||||
name: "rows",
|
||||
isArray: true,
|
||||
default: []
|
||||
},
|
||||
{
|
||||
name: "columns",
|
||||
isArray: true,
|
||||
default: []
|
||||
}
|
||||
]);
|
||||
|
@ -45,7 +45,12 @@ export default function init (Survey) {
|
||||
Survey.JsonObject.metaData.addProperties("selectbox", [
|
||||
{
|
||||
name: "choices",
|
||||
isArray: true,
|
||||
default: []
|
||||
},
|
||||
{
|
||||
name: "multipleAnswer",
|
||||
default: true
|
||||
}
|
||||
]);
|
||||
},
|
||||
@ -54,7 +59,7 @@ export default function init (Survey) {
|
||||
isDefaultRender: 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
|
||||
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>`;
|
||||
}
|
||||
|
||||
let selectDOM = el.querySelector("select");
|
||||
selectDOM.innerHTML = optionsHTML;
|
||||
let additionalAttr = "";
|
||||
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) => {
|
||||
let i;
|
||||
let opts = new Array(e.currentTarget.selectedOptions.length);
|
||||
|
@ -17,15 +17,22 @@ class SideBySideMatrix
|
||||
// INCLUDING those added/modified by application's code.
|
||||
const surveyCSS = cfg.question.css;
|
||||
this._CSS_CLASSES = {
|
||||
WRAPPER: surveyCSS.matrix.tableWrapper,
|
||||
WRAPPER: `${surveyCSS.matrix.tableWrapper} sbs-matrix`,
|
||||
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: 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;
|
||||
@ -71,7 +78,10 @@ class SideBySideMatrix
|
||||
{
|
||||
bodyCells +=
|
||||
`<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>`;
|
||||
}
|
||||
return bodyCells;
|
||||
@ -85,7 +95,14 @@ class SideBySideMatrix
|
||||
{
|
||||
bodyCells +=
|
||||
`<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>`;
|
||||
}
|
||||
return bodyCells;
|
||||
@ -168,7 +185,10 @@ class SideBySideMatrix
|
||||
// TODO: Find out how it actually composed inside SurveyJS.
|
||||
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 headerCells = "";
|
||||
@ -189,7 +209,10 @@ class SideBySideMatrix
|
||||
</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>`;
|
||||
subHeaderCells += `<th
|
||||
class="${CSS_CLASSES.TABLE_HEADER_CELL} sbs-matrix-header-cell--${question.columns[i].cellType}">
|
||||
${question.columns[i].subColumns[j].text}
|
||||
</th>`;
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -198,7 +221,7 @@ class SideBySideMatrix
|
||||
`<th class="${CSS_CLASSES.TABLE_HEADER_CELL}">
|
||||
${question.columns[i].title}
|
||||
</th>`;
|
||||
subHeaderCells += "<td></td>";
|
||||
subHeaderCells += `<td class="${CSS_CLASSES.TABLE_HEADER_CELL} sbs-matrix-header-cell--${question.columns[i].cellType}"></td>`;
|
||||
}
|
||||
headerCells += "<td></td>";
|
||||
subHeaderCells += "<td></td>";
|
||||
@ -227,8 +250,8 @@ class SideBySideMatrix
|
||||
|
||||
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>
|
||||
<tr><td></td><td></td>${headerCells}</tr>
|
||||
<tr><td></td><td></td>${subHeaderCells}</tr>
|
||||
</thead>
|
||||
<tbody>${bodyHTML}</tbody>
|
||||
</table>`;
|
||||
@ -293,10 +316,12 @@ export default function init (Survey) {
|
||||
Survey.JsonObject.metaData.addProperties("sidebysidematrix", [
|
||||
{
|
||||
name: "rows",
|
||||
isArray: true,
|
||||
default: []
|
||||
},
|
||||
{
|
||||
name: "columns",
|
||||
isArray: true,
|
||||
default: []
|
||||
}
|
||||
]);
|
||||
|
@ -6,6 +6,11 @@ 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;
|
||||
@ -102,7 +107,7 @@ class SliderStar
|
||||
<div class="star-slider-inputs">
|
||||
<div class="stars-container" data-idx="${question.choices[i].value}">${starsHTML}</div>
|
||||
${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>`;
|
||||
@ -166,6 +171,7 @@ export default function init (Survey) {
|
||||
Survey.JsonObject.metaData.addProperties("sliderstar", [
|
||||
{
|
||||
name: "choices",
|
||||
isArray: true,
|
||||
default: []
|
||||
},
|
||||
{
|
||||
|
@ -44,6 +44,7 @@ export default function init (Survey) {
|
||||
Survey.JsonObject.metaData.addProperties("slider", [
|
||||
{
|
||||
name: "choices",
|
||||
isArray: true,
|
||||
default: []
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user