Перейти к основному содержимому

Сборник готовых скриптов

На этой странице собраны примеры различных скриптов, на основе которых можно писать свои. Перед использованием скрипта из этого списка рекомендуем проверить правильность его работы.

Автоматическое кодирование возраста

Пример перенесён в статью.

Вычисление возраста по дате рождения

Q1 - текстовый вопрос с флагом Для открытого текстового значения использовать выбор даты. В числовом вопросе — скрипт перед показом:

Q.openValueNum = getAge(Q1.openValueTxt);

return answered;

// --------------------
function getAge(date) {
let birth;
if (date.indexOf('/') > -1) birth = new Date(date);
if (date.indexOf('.') > -1) {
let arr = date.split('.');
birth = new Date(arr[2], arr[1] - 1, arr[0]);
}

let ageDate = new Date(Date.now() - birth.getTime());

return Math.abs(ageDate.getUTCFullYear() - 1970);
}

Вывести на экран имя респондента

В текстовый вопрос добавить скрипт после ответа:

informationText('Имя респондента: ' + Q.openValueTxt);

Показать выбранные в предыдущем вопросе ответы с переносом текста из «другого»

Q1 - вопрос с выбором, некоторые ответы содержат текстовые поля. В следующем вопросе должен быть такой же список ответов, но без текстовых полей, и скрипт перед показом:

// можно менять на 'rows' или 'columns' в зависимости от того, что нужно показать
let objName = 'answers';

Q[objName].hideAll();

for (let a of Q1.getChecked()) {
Q[objName].show(a.code);

if (a.flags & AnswerFlags.OpenValueTxt) {
Q[objName][a.code].text = a.openValueTxt;
}
}

return Q[objName].visibleCount > 0 ? ok : skip;

Автоматически выбрать единственный доступный ответ

Пример перенесён в статью.

Требовать ответ в необязательной для заполнения строке таблицы с выбором, если заполнено текстовое поле

В свойствах строки должны стоять флаги Проверка ответа скриптами и С открытым значением (текст). Добавить скрипт после ответа:

let rFlags = AnswerFlags.CustomRowValidation | AnswerFlags.OpenValueTxt;

for (let row of Q.rows.getVisible()) {
if ((row.flags & rFlags) != rFlags) continue;

if (row.openValueTxt && row.getCheckedCodes().length == 0) {
return error('Необходимо выбрать ответ в строке ' + row.code);
}
}

Собственная подстановка ответа в вопросах цикла (вместо {answerText})

Если стандартный макрос {answerText} по каким-то причинам не подходит, то можно использовать глобальную переменную (имя - на своё усмотрение). Здесь используется {Магазин}. Q1 - вопрос, по ответам (1-98) которого задаётся Q2. Глобальный скрипт перед показом:

if ((Q.number / 100 | 0) == 2) {
let a = Q1[Q.number % 100];
V['Магазин'] = a.flags & AnswerFlags.OpenValueTxt ? a.openValueTxt : a.text;
}

Вставить текущее время в формате «DD.MM.YY HH:mm:ss»

Добавьте в текстовый вопрос скрипт перед показом:

if (Q.isAnswered) return answered;

let d = new Date();
let time = [
'0' + d.getDate(),
'0' + (d.getMonth() + 1),
d.getFullYear().toString(),
'0' + d.getHours(),
'0' + d.getMinutes(),
'0' + d.getSeconds()
].map(component => component.slice(-2));

Q.openValueTxt = time.slice(0,3).join('.') + ' ' + time.slice(-3).join(':');

return answered;
предупреждение

В веб-версии анкеты время будет московское, в приложении — в часовом поясе устройства.

Выбрать случайные варианты ответа

В переменной num нужно указать желаемое количество случайных ответов. Если требуется делать выбор только среди определённых ответов, то их нужно сделать видимыми сразу после первого if, скрыв лишние. Скрипт перед показом:

let num = 1;

if (Q.isAnswered) return answered;

if (Q.visibleCount == 0) return skip;

for (let A of randomizeArray(Q.getVisible()).slice(0, num)) {
A.checked = true;
}

return answered;

Выбрать один случайный ответ с заданной вероятностью

В функцию getRandom() передаются массив кодов ответов текущего вопроса с единственным выбором (среди которых нужно выбирать) и массив вероятностей в таком же порядке, как коды. В примере ниже код 1 выбирается с вероятностью 60%, коды 2 и 3 - с вероятностью 20%. Точность распределения будет зависеть от размера выборки.

if (Q.isAnswered) return answered;

let code = getRandom([1, 2, 3], [0.6, 0.2, 0.2]); // сумма элементов второго массива должна быть равна 1

Q[code].checked = true;

return answered;

function getRandom(codes, weights) {
if (codes === undefined || weights === undefined) return;

let num = Math.random();
let s = 0;
let lastIndex = weights.length - 1;

for (let i = 0; i < lastIndex; ++i) {
s += weights[i];
if (num < s) {
return codes[i];
}
}

return codes[lastIndex];
}

Сгенерировать случайное число в указанном диапазоне

В функцию getRandomFromTo() передаётся начало и окончание диапазона:

if (Q.isAnswered) return answered;

Q.openValueNum = getRandomFromTo(1, 10);

return answered;

function getRandomFromTo(min, max) {
let range = max - min + 1;

return Math.floor(Math.random()*range) + min;
}

Сгенерировать уникальный идентификатор

При каждом вызове функции getUUID() генерируется идентификатор UUID вида 9f10fddb-4a95-4849-87d0-3e03cb1700f9:

if (Q.isAnswered) return answered;

Q.openValueTxt = getUUID();

return answered;

function getUUID() {
let d = new Date().getTime();

return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);

return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}

Перенести (скопировать) ответы из одного вопроса с выбором в другой

Это расширенный аналог действия Перенести ответы из другого вопроса для вопросов с выбором. Коды ответов в обоих вопросах должны совпадать. Если код не найден - он будет пропущен. В функцию можно передать список кодов, которые переносить не нужно. Скрипт перед показом перенесёт в текущий вопрос ответы из Q1 и Q7:

Q.reset();

copyAnswers(1, [97,99]); // кроме кодов 97 и 99
copyAnswers(7);

return Q.isAnswered ? answered : skip;

function copyAnswers(qn, except) {
except = except === undefined ? [] : except;

for (let a of questions[qn].getChecked()) {
if (except.indexOf(a.code) > -1 || !(a.code in Q)) continue;

let qA = Q[a.code];

qA.checked = true;

if (qA.flags & AnswerFlags.openValueNum) {
qA.openValueNum = a.openValueNum;
}

if (qA.flags & AnswerFlags.OpenValueTxt) {
qA.openValueTxt = a.openValueTxt;
}
}
}

Разрешить выбор не более 2-х вариантов ответа с кодом 99 среди всех вопросов анкеты

Глобальный скрипт после ответа:

if (Q.isAnswered) {
let code = 99;
let count = 0;
let qnList = questions.getNumbers();
let startIdx = qnList.indexOf(Q.number);

for (let i = startIdx; i >= 0; i--) {
let qx = questions[qnList[i]];

switch (qx.type) {
case QuestionTypeIds.SingleChoice:
case QuestionTypeIds.MultipleChoice:
case QuestionTypeIds.Dropdown_SingleChoice:
case QuestionTypeIds.Dropdown_MultipleChoice:
case QuestionTypeIds.ClickTest_Text:
if (qx.isChecked(code)) count++;
break;

case QuestionTypeIds.Table_SingleChoice:
case QuestionTypeIds.Table_MultipleChoice:
qx.rows.getVisible().forEach(function(row) {
if (row.isChecked(code)) count++;
});
break;
}

if (count > 2) return error('В анкете выбрано более 2-х ответов ' + code);
}
}

Выводить текст вопроса в зависимости от выбранного в предыдущем вопросе ответа

Q1 - единственный выбор. В следующем вопросе скрипт перед показом:

let code = Q1.getCheckedCode();
let texts = {
1: 'Текст вопроса для кода 1',
2: 'Текст вопроса для кода 2',
3: 'Текст вопроса для кода 3'
};

Q.text = code in texts ? texts[code] : 'Не найден текст для кода ' + code;

Фильтровать ответы по выбранному в указанном вопросе ответу

Этот скрипт перед показом удобно использовать вместе с большим списком ответов, в котором коды неупорядочены. Для небольшого списка с упорядоченными кодами удобнее использовать действия Показать варианты ответа с кодами в указанном диапазоне и Показать указанные варианты ответа. В функцию showOnlyCodesByCode() можно передавать как один код, так и массив кодов.

// можно менять на 'rows' или 'columns' в зависимости от того, что нужно показать
let objName = 'answers';

let codes = {
1: [ 1456 ], // если выбран код 1, то показать код 1456
12: [ [1449, 1455] ], // если выбран код 12 - показать все коды с 1449 по 1455
34: [ 290, [815, 877], 916 ], // выбран 34 - показать 290, 916 и все с 815 по 877
};

showOnlyCodesByCode(Q560.getCheckedCode());

return Q[objName].visibleCount > 0 ? ok : skip;

// ----------------------------------------
function showOnlyCodesByCode(parameter) {
Q[objName].hideAll();

switch (typeof parameter) {
case 'number':
showAnswers(parameter);
break;

case 'object':
for (let code of parameter) {
showAnswers(code);
}
break;
}

function showAnswers(code) {
if (!(code in codes)) return;

for (let elem of codes[code]) {
switch (typeof elem) {
case 'number':
Q[objName].show(elem);
break;

case 'object':
Q[objName].showFromTo(elem[0], elem[1]);
break;
}
}
}
}

Ранжирование вариантов ответа вопросами

Q1 - вопрос с единственным выбором и списком ответов, которые нужно ранжировать. Перед ним потребуется служебный вопрос, например Q8001, с последовательными кодами ответов (1,2,3… - по количеству ранжируемых ответов) и невыполнимым условием показа, например 1 = 2 или просто false. Ему также можно поставить флаг Исключить вопрос при выгрузке.

В тексте вопроса Q1 можно использовать подстановку {answerCode}, например «{answerCode} место:». Далее в Подготовке создаём цикл:

questions.repeatIfNot(1, 1, 8001);

В глобальный скрипт перед показом нужно добавить:

let div = 100;
let qn = Q.number / div | 0;
let code = Q.number % div;

if (qn == 1) {
Q.showAll();

for (let aCode of Q8001.getCodes()) {
if (aCode == code) break;

Q.hide(questions[qn * div + aCode].getCheckedCode());
}

if (Q.visibleCount == 1) {
Q[Q.getVisibleCodes()[0]].checked = true;
return answered;
}
}

Если максимальный код ответа в Q8001 не двузначный, в переменной div нужно изменить делитель.

Выводить указанное в предыдущем вопросе количество строк таблицы

Q1 - числовой вопрос, следующий - табличный с максимально необходимым количеством строк (коды значения не имеют). В нём скрипт перед показом:

let num = Q1.openValueInt;
let rowCodes = Q.rows.getCodes();

Q.rows.hideAll();

for (let i = 0, max = rowCodes.length; i < num && i < max; i++) {
Q.rows.show(rowCodes[i]);
}

return Q.rows.hasVisible ? ok : skip;

Скрыть флажок или переключатель, чтобы ответ нельзя было выбрать

Скрывает все теги input для кодов ответов, которые начинаются с числа 500 (5001, 500, 50010…). Этот скрипт во время показа может не работать в устаревших браузерах.

В простых вопросах с выбором:

$('input[id*="c500"]').parent().remove();

В табличных - удаление ответов (колонок):

$('input[id*="c500"]').parent().remove();
fitTableSizes();

В табличных - удаление всех ответов из строк (в строке не будет ответов для выбора):

$('input[id*="r500"]').parent().remove();
fitTableSizes();

Выбор варианта ответа с наименьшим значением счётчика

Скрипт перед показом проходит по всем ответам текущего вопроса и ищет счётчики с названиями вариантов ответа. Затем выбирает указанное в первой строке количество ответов с наименьшим значением счётчика. Если в счётчике указано значение квоты и она закрыта - счётчик пропускается. Скрипт может работать с любым количеством ответов.

let num = 1; // количество ответов

if (Q.isAnswered) return answered;

let codes = [];

for (let a of Q.getVisible()) {
let counterName = a.plainText;
let counter = getCounter(counterName);

if (counter === undefined) {
Q.comment = 'Произошла ошибка! Не найден счётчик «' + counterName + '»';
if (isTesting()) Q.comment += '<br />В режиме тестирования анкеты счётчики недоступны - ответ нужно выбирать вручную.';
return ok;
}

if (counter.quota >= 0 && counter.value + 1 > counter.quota) continue;

codes.push({'code': a.code, 'value': counter.value});
}

if (codes.length == 0) return exitWithResult(InterviewResult.Overquoting, 'Квоты в проверяемых счётчиках закрыты.');

codes.sort((a, b) => a.value - b.value);

for (let item of codes.slice(0, num)) {
Q[item.code].checked = true;
}

return answered;

Проверить правильность введённого номера телефона, но разрешить вводить 99 при отказе от ответа

Если вопрос числовой, то нужно добавить в него такой скрипт после ответа:

let phone = Q.openValueNum;

if (phone == 99) return ok;

if (phone < 80000000000 || phone > 89999999999) {
return error('Телефон должен начинаться с 8 и содержать 11 цифр. ' +
'В случае отказа, введите 99.');
}

Если вопрос табличный текстовый, с телефоном в первой строке:

let row = Q.rows[1].answer;

if (row.openValueTxt === undefined) return ok;

let phone = row.openValueTxt.replace(/\D/g, '');

phone = parseInt(phone, 10);

if (phone == 99) return ok;

if (isNaN(phone) || phone < 80000000000 || phone > 89999999999) {
return error('Телефон должен начинаться с 8 и содержать 11 цифр. ' +
'Остальные символы будут удалены. В случае отказа, ' +
'введите 99.');
}

row.openValueTxt = phone;

Проверка суммы введённых чисел

В табличный числовой вопрос добавить скрипт после ответа:

let sum = 100;
let total = 0;

for (let row of Q.rows.getVisible()) {
let num = row.answer.openValueNum;

total += num ? num : 0;
}

total = +total.toFixed(2);

if (total != sum) {
return error('Сумма значений не равна ' + sum + '. Сейчас ' + total);
}

Добавить изображения к вариантам ответа или строкам таблицы

Функция из статьи «Добавление картинок в анкету».

function addImages(Q, name, placement) {
let objName;
switch (Q.type) {
case QuestionTypeIds.SingleChoice:
case QuestionTypeIds.MultipleChoice:
case QuestionTypeIds.Ranking:
objName = 'answers';
break;

case QuestionTypeIds.Table_Text:
case QuestionTypeIds.Table_Numeric:
case QuestionTypeIds.Table_SingleChoice:
case QuestionTypeIds.Table_MultipleChoice:
case QuestionTypeIds.Table_Rating:
case QuestionTypeIds.Table_Slider:
case QuestionTypeIds.GrouppingAndRanking:
objName = 'rows';
break;

case QuestionTypeIds.MaxDiff:
objName = 'columns';
break;

default:
return;
}

if (name === undefined) name = '';

for (let A of Q[objName].getVisible()) {
if (A.image !== undefined) continue;

A.image = images[name + A.code];
A.imagePlacement = placement ? placement : ImagePlacementIds.Default;
}
}

Установить всем картинкам у вариантов ответа ширину 150px

Добавьте в CSS:

.ss-answers-clickable-img > img {
width: 150px;
}

Подстановка ответа (числового кода) из метки базы контактов или из указанного поля

Скрипт из статьи «Перенос ответа на вопрос из базы контактов».

if (isTesting()) return ok;
if (isPostProcessing() || isValidation()) return answered;

let tag = contact.tag;
let name = 'ИМЯ ПОЛЯ';

if (tag === undefined) {
tag = contact.data[name];
}

if (tag === undefined) {
informationTextAdd('ВНИМАНИЕ! Не найдена метка ни в свойствах базы ' +
'контактов, ни в её поле «{0}».', name);
return ok;
}

let code = parseInt(tag, 10);
if (isNaN(code)) {
informationTextAdd('ВНИМАНИЕ! Ошибка в формате метки базы контактов ' +
'(допускается число, а там «{0}»).', tag);
return ok;
}

if (Q[code] === undefined) {
informationTextAdd('ВНИМАНИЕ! Отсутствует ответ с кодом {0}, который ' +
'указан в качестве метки базы контактов.', code);
return ok;
}

Q[code].checked = true;

return answered;

Подстановка ответа (числового кода) из указанного поля базы контактов

Скрипт из статьи «Перенос ответа на вопрос из базы контактов».

if (isTesting()) return ok;
if (isPostProcessing() || isValidation()) return answered;

let name = 'ИМЯ ПОЛЯ';
let value = contact.data[name];

if (value === undefined) {
informationTextAdd('ВНИМАНИЕ! В поле «{0}» базы контактов отсутствует значение', name);
return ok;
}

let code = parseInt(value, 10);

if (isNaN(code)) {
informationTextAdd('ВНИМАНИЕ! Ошибка в формате значения «{0}» базы контактов ' +
'(допускается число, а там «{1}»).', name, value);
return ok;
}

if (Q[code] === undefined) {
informationTextAdd('ВНИМАНИЕ! Отсутствует ответ с кодом {0}, который ' +
'указан в поле {1} базы контактов.', code, name);
return ok;
}

Q[code].checked = true;

return answered;

Кодирование текста из поля базы контактов

Скрипт из статьи «Перенос ответа на вопрос из базы контактов».

if (isTesting()) return ok;
if (isPostProcessing() || isValidation()) return answered;

let name = 'ИМЯ ПОЛЯ';
let value = contact.data[name];

if (value === undefined) {
informationTextAdd('ВНИМАНИЕ! В поле «{0}» базы контактов нет текста', name);
return ok;
}

for (let a of Q.getAll()) {
if (a.plainText.toUpperCase() == value.trim().toUpperCase()) {
a.checked = true;
return answered;
}
}

informationTextAdd('ВНИМАНИЕ! Отсутствует ответ для текста «{0}», \
который указан в поле «{1}» базы контактов.', value, name);

Завершить интервью, если выбран только ответ 3, но продолжить, если он выбран вместе с другим ответом

Это можно сделать простым действием Завершить интервью после ответа с обычным выражением в условии. Но можно и скриптом после ответа:

if (Q.getCheckedCodes().length == 1 && Q.isChecked(3)) {
return exit();
}

Показать ответы или строки, для которых в строках табличного вопроса выбраны ответы с кодом 1 или 3

Q1 - табличный вопрос с выбором. В следующем вопросе должен быть такой же список ответов или строк и скрипт перед показом:

// можно менять на 'answers' или 'columns' в зависимости от того, что нужно показать
let objName = 'rows';

Q[objName].hideAll();

for (let row of Q1.rows.getVisible()) {
if ( [1,3].some(code => row.isChecked(code)) ) {
Q[objName].show(row.code);
}
}

return Q[objName].visibleCount > 0 ? ok : skip;

Добавить необходимые подстановки в имена переменных массива

Если в анкете нет каких-то особых требований к именам переменных, то можно добавить этот скрипт в Подготовку и во втором поле свойств вопроса указывать только код вопроса без большинства подстановок, описанных в этой статье. Например, к переменной ABC12 в вопросе с множественным выбором при выгрузке массива скрипт добавит _{1}, а также в числовую и текстовую переменные - ABC12_{1}N и ABC12_{1}T, соответственно. Разделители ответов и строк таблицы можно менять в переменных ansDlmtr и rowsDlmtr.

Если вопрос находится внутри цикла - подстановку {3} нужно обязательно прописывать вручную, например ABC12.{3}.

if (isExport()) {
for (let q of questions.getAll()) {
let tmp = q.outputColumnTemplate;

if (tmp === undefined) continue;

if (q.flags & QuestionFlags.SkipExport) {
q.outputColumnTemplate = undefined;
continue;
}

if (tmp[0] === "'") {
q.outputColumnTemplate = tmp.slice(1);
continue;
}

q.outputColumnTemplate = getTemplate(q.type, tmp);
q.outputColumnTemplateOVN = getTemplate(q.type, tmp, 'N');
q.outputColumnTemplateOVT = getTemplate(q.type, tmp, 'T');
}
}

// --------------------------------------------------------------
function getTemplate(qType, tmp, oV) {
let ansDlmtr = '_';
let rowsDlmtr = '.';

switch (qType) {
case QuestionTypeIds.SingleChoice:
case QuestionTypeIds.Dropdown_SingleChoice:
tmp += oV === undefined ? '' : ansDlmtr + '{1}' + oV;
break;

case QuestionTypeIds.MultipleChoice:
case QuestionTypeIds.Dropdown_MultipleChoice:
case QuestionTypeIds.Ranking:
case QuestionTypeIds.ClickTest_Text:
tmp += oV === undefined ? ansDlmtr + '{1}' : ansDlmtr + '{1}' + oV;
break;

case QuestionTypeIds.Table_Text:
case QuestionTypeIds.Table_Numeric:
case QuestionTypeIds.Table_Rating:
case QuestionTypeIds.Table_Slider:
case QuestionTypeIds.GrouppingAndRanking:
tmp += oV === undefined ? rowsDlmtr + '{2}' : rowsDlmtr + '{2}' + oV;
break;

case QuestionTypeIds.Table_SingleChoice:
case QuestionTypeIds.MaxDiff:
tmp += oV === undefined ? rowsDlmtr + '{2}' : rowsDlmtr + '{2}' + ansDlmtr + '{1}' + oV;
break;

case QuestionTypeIds.Table_MultipleChoice:
tmp += oV === undefined ? rowsDlmtr + '{2}' + ansDlmtr + '{1}' : rowsDlmtr + '{2}' + ansDlmtr + '{1}' + oV;
break;
}

return tmp;
}

Показывать имя переменной в тексте вопроса в режиме тестирования анкеты

Глобальный скрипт перед показом:

if (isTesting()) {
let tmp = Q.outputColumnTemplate;
if (tmp !== undefined) {
tmp = tmp.replace(/([._]|){\d}|'/g, '');

if (Q.text.indexOf(tmp) == -1) {
Q.text = '<font color="gray">' + tmp + '</font><br />' + Q.text;
}
}
}

Создать цикл внутри цикла

Допустим, вопросы Q2-Q4 нужно задать по выбранным в Q1 ответам. При этом вопрос Q3 нужно задать по выбранным ответам в Q2.

Таких конструкций в анкетах лучше избегать. Из-за вложенных циклов можно легко получить 1.000+ вопросов - анкета может работать медленно. А в массиве при этом создаются тысячи колонок. Например, вместо второго цикла можно попробовать табличные вопросы, в том числе с принудительной адаптацией. Однако если это всё-таки необходимо, можно брать за основу эти скрипты.

Подстановка {3} в имени переменной будет корректно работать только для основного цикла. Имя для вопроса вложенного цикла прописывается функциями после горизонтальной черты. В большинстве случаев их менять не нужно. В самом вопросе нужно только вписать желаемое имя, а все необходимые подстановки добавит функция setTemplates(). Если имена переменных не нужны, эту функцию использовать не надо.

предупреждение

Скрипты в вопросах, находящихся внутри двойного цикла, не выполняются — для них используйте глобальные скрипты.

Скрипт Подготовка:

questions.repeat(2, 4, 1);

for (let a of Q1.getAll()) {
if ((a.flags & AnswerFlags.DisableRepeat) == 0) {
let q2x = 200 + a.code;
let q3x = 300 + a.code;

questions.repeat(q3x, q3x, q2x);
setTemplates(q2x, q3x, a.code, 100);
}
}

// --------------------------------------------------------------------
function setTemplates(qSourceN, qTargetN, mainAnswerCode, multiplier) {
for (let a of questions[qSourceN].getAll()) {
if ((a.flags & AnswerFlags.DisableRepeat) == 0) {
let q = questions[qTargetN * multiplier + a.code];
let tmp = q.outputColumnTemplate;

q.outputColumnTemplate =
getTemplate(q.type, tmp, mainAnswerCode, a.code);
q.outputColumnTemplateOVN =
getTemplate(q.type, tmp, mainAnswerCode, a.code, 'N');
q.outputColumnTemplateOVT =
getTemplate(q.type, tmp, mainAnswerCode, a.code, 'T');
}
}
}

function getTemplate(qType, tmp, code1, code2, oV) {
tmp += '.' + code1 + '.' + code2;

switch (qType) {
case QuestionTypeIds.SingleChoice:
case QuestionTypeIds.Dropdown_SingleChoice:
tmp += oV === undefined ? '' : '_{1}' + oV;
break;

case QuestionTypeIds.MultipleChoice:
case QuestionTypeIds.Dropdown_MultipleChoice:
case QuestionTypeIds.Ranking:
case QuestionTypeIds.ClickTest_Text:
tmp += oV === undefined ? '_{1}' : '_{1}' + oV;
break;

case QuestionTypeIds.Table_Text:
case QuestionTypeIds.Table_Numeric:
case QuestionTypeIds.Table_Rating:
case QuestionTypeIds.Table_Slider:
tmp += oV === undefined ? '.{2}' : '.{2}' + oV;
break;

case QuestionTypeIds.Table_SingleChoice:
case QuestionTypeIds.MaxDiff:
tmp += oV === undefined ? '.{2}' : '.{2}_{1}' + oV;
break;

case QuestionTypeIds.Table_MultipleChoice:
tmp += oV === undefined ? '.{2}_{1}' : '.{2}_{1}' + oV;
break;
}

return tmp;
}

Глобальный скрипт перед показом:

let qn = Q.number / 100 | 0;
let code = Q.number % 100;

if (qn == 2 || qn == 4) {
let a = Q1[code];

V['Ответ из Q1'] = a.flags & AnswerFlags.OpenValueTxt ? a.openValueTxt : a.text;
}

if ((qn / 100 | 0) == 3) {
let a = questions[200 + qn % 100][code];

V['Ответ из Q2'] = a.flags & AnswerFlags.OpenValueTxt ? a.openValueTxt : a.text;
}

Вывести номера всех вопросов с их условиями показа

В первый вопрос нужно добавить скрипт перед показом:

informationTextClear();
for (let q of questions.getAll()) {
informationTextAdd("Q{0}: {1}", q.number, q.condition);
}

Запретить повторное заполнение анкеты в веб-опросе

При анонимном опросе идентифицировать респондента на 100% невозможно, поэтому у него всегда останется возможность обойти ограничение. Блокировать анонимного пользователя можно по его IP-адресу и браузеру.

Добавьте в скрипт перед показом первого вопроса анкеты:

if (isPostProcessing() || isValidation()) return ok;

V.key = interview.ipAddress + interview.userAgent;

if (isKeyLocked(V.key)) return exitWithResult(InterviewResult.Exited);

В глобальный скрипт Обработка добавьте:

if (isValidation()) return ok;

switch (interview.result) {
case InterviewResult.Exited:
case InterviewResult.Completed:
case InterviewResult.Screening:
case InterviewResult.Overquoting:
lockKey(V.key);
break;
}
к сведению

Так как по умолчанию интервью, превысившее квоту, не сохраняется в базу данных, скрипт Обработка выполняться не будет – блокировка не сработает. Поэтому нужно включить сохранение таких интервью.

Запретить переход к следующему вопросу на определённое время

Если опрос будет в браузере, можно скрыть кнопку Далее скриптом во время показа (может не работать в устаревших браузерах):

var time = 10; // время в секундах

var conditions = [
$('.validation-summary-errors').length == 0,
$('input:radio:checked').length == 0,
$('input:checkbox:checked').length == 0,
$.map($('input:text[id*="_ov"]'), (i) => $(i).val().length).every((e) => e == 0)
];

if (conditions.indexOf(false) == -1) {
var $nextBtn = $('#processingGoForwardBtn');
var html = $nextBtn.html();
var text = $nextBtn.text().trim();

$nextBtn.attr('disabled', true);
$nextBtn.text(text + ' (' + time + ')');

setTimeout(function qTimer() {
$nextBtn.text(text + ' (' + --time + ')');

if (time > 0) {
setTimeout(qTimer, 1000);
} else {
$nextBtn.attr('disabled', false);
$nextBtn.html(html);
}
}, 1000);
}

Запрет в приложении можно сделать двумя скриптами, перед показом:

if (isPostProcessing()) return ok;

V.now = (new Date()).getTime();

и после ответа:

if (isPostProcessing() || !V.now) return ok;

let time = 10; // время в секундах
let startTime = Number(V.now);
let duration = (new Date()).getTime() - startTime;

if (duration >= time*1000) return ok;

let left = time - duration / 1000;

return error('Продолжение возможно через ' + left.toFixed(0) + ' сек.');

Требовать ответ только в указанном количестве строк табличного вопроса

В свойствах вопроса необходимо включить флаг Проверка ответа скриптами и добавить скрипт после ответа:

let min = 2; // количество строк с ответом
let count = 0;

if (min > Q.rows.visibleCount) min = Q.rows.visibleCount;

for (let row of Q.rows.getVisible()) {
switch (Q.type) {
case QuestionTypeIds.Table_Text:
if (row.answer.openValueTxt !== undefined) count++;
break;

case QuestionTypeIds.Table_Numeric:
case QuestionTypeIds.Table_Rating:
if (row.answer.openValueNum !== undefined) count++;
break;

case QuestionTypeIds.Table_SingleChoice:
case QuestionTypeIds.Table_MultipleChoice:
if (row.getCheckedCodes().length > 0) count++;
break;
}

if (count >= min) return ok;
}

return error('Требуется заполнить не менее ' + min + ' строк');

Проверка номера текущего вопроса в глобальном скрипте во время показа

var qn = Number(document.getElementById('ss_current_qn').value);

if (qn == 777) {
// что-то сделать
}

Автоматически выбрать ответ 2, если в Q1 выбран ответ 1, и вопрос на экран не выводить

Предполагается, что оба вопроса с выбором (единственным или множественным - не важно). Скрипт перед показом:

if (calc('Q1 = 1')) {
Q[2].checked = true;
return answered;
}

Увеличить картинку по клику в браузере

В свойстве max-width нужно указать ширину маленького изображения, в процентах от исходного. Скрипт во время показа (может не работать в устаревших браузерах):

$('#questionImage').click(function() {
showFullScreenImage($(this).attr('src'));
}).css({
'cursor': 'pointer',
'max-width': '30%' // уменьшенный размер картинки
}).removeClass('mw-100');

Изменить текст сообщения при срабатывании квоты

В скрипте Обработка:

if (interview.result == InterviewResult.Overquoting) {
return exit('Достигнут лимит по квоте ('+ interview.resultDetails +'). Спасибо, до свидания!');
}

Проверить минимальную длину вписанного текста

Пример находится в статье.

Ротация произвольного массива

Если вы ищете информацию о ротации вариантов ответа — читайте эту статью.

Для ротации любого массива добавьте в анкету функцию:

function rotate(array) {
let idx = (rotationCounter - 1) % array.length;
idx *= -1; // Ротация справа налево (как в SURVEYSTUDIO)

return [].concat(
array.slice(idx, array.length),
array.slice(0, idx)
);
}

Пример использования:

informationText(rotate([1,2,3,4,5,6,7,8,9,10]));

Архив

Сюда перемещаются скрипты, в которых больше нет необходимости — оставлены просто для вдохновления.

Замер времени ответов на вопросы

Время теперь записывается всегда. Добавить его в массив можно флагом Выгружать длительность ответа на вопрос при выгрузке.

Глобальный скрипт Подготовка (если есть циклы по вопросам - вставлять в конец скрипта):

// --------------------------------------------------------------
// 0 - не добавлять время в массив, 1 - добавлять
let enableExport = 1;
// номер первого вопроса анкеты, перед которым будет время ответов
let FQn = questions.getNumbers()[0];
// вставляем вопрос, в котором будем хранить время ответов
let QT = questions.insert(FQn, 12345678, 'Замер времени', '', 'multiplechoice');
QT.outputColumnTemplateOVN = 'Q{1}_TIME';
QT.flags = QuestionFlags.SkipExport;
if (enableExport) QT.flags |= QuestionFlags.KeepExportOV;
// добавляем ответы для замера времени для каждого вопроса
questions.getNumbers().forEach(function (qn) {
if (qn != QT.number) {
QT.answers.add(qn, "Время ответа на Q" + qn).flags = 0x0001 | 0x0800;
}
});
// --------------------------------------------------------------

Глобальный скрипт перед показом:

// --------------------------------------------------------------
if (Q.number == 12345678) return Q.isAnswered ? answered : skip;

let qtimeVar = "QTIME_" + Q.number;
/**
* если ниже добавить && !V[qtimeVar] - в начатый вопрос будет прибавляться
* время на возврат назад; если && !Q.isAnswered - не будет времени для
* информационных и с флагом "Не требовать обязательного ответа…"
*/
if (!isPostProcessing()) {
V[qtimeVar] = (new Date()).getTime();
}
// --------------------------------------------------------------

Глобальный скрипт после ответа:

// --------------------------------------------------------------
let qtimeVar = "QTIME_" + Q.number;
if (!isPostProcessing() && Q.isAnswered && V[qtimeVar]) {
// время показа вопроса
let startTime = Number(V[qtimeVar]);
// вопрос, в котором храним время
let QT = questions[12345678];
// для всех вопросов, кроме того, в котором храним время
if (Q.number != QT.number) {
let duration = (new Date()).getTime() - startTime;
let A = QT[Q.number];
if (A && !A.checked) {
A.checked = true;
A.openValueNum = duration / 1000;
}
}
}
// --------------------------------------------------------------

Перемешать варианты ответа блоками

Теперь у списков ответов, строк и колонок есть метод randomizeGroups() — лучше использовать его.

Q1 - вопрос с выбором, в котором нужно перемешать 3 блока ответов. Для этого в Подготовку необходимо добавить всё, что после черты ниже, и там же вызвать функцию shuffleBlocks(вопрос, типСписка, массив, [true/false]) с необходимыми аргументами. Третий аргумент, массив, может содержать либо массивы с диапазонами ответов (два значения: «с», «по»), либо все ответы каждого блока. Если массив содержит коды ответов, то нужно добавить четвёртый аргумент – true. Примеры:

shuffleBlocks(1,
'answers',
[[5001,9], [5002,14], [5003,20]]);

// или
shuffleBlocks(1,
'answers',
[[5001,1,2,3,7,9], [5002,12,13,14], [5003,19,20]],
true);

// –––––––––––––––––––––––––––––––––––––––––––––––––––
function shuffleBlocks(qn, objName, array, hasCodes) {
let qX = questions[qn];
let blocks = hasCodes ? array : getBlocks(qX[objName].getCodes(), array);
let codes = [];

for (let block of randomizeArray(blocks)) {
codes = codes.concat(block);
}

qX[objName].setOrder(codes);

function getBlocks(codes, array) {
let arr = [];

for (let range of array) {
let from = codes.indexOf(range[0]);
let to = codes.indexOf(range[1]);

arr.push(codes.slice(from, to+1));
}

return arr;
}
}

Рандомизация вопросов группами разного размера

Стандартный метод randomizeGroups() теперь позволяет перемешивать группы вопросов разного размера — лучше использовать его.

Стандартный метод randomizeGroups() позволяет перемешивать группы, только если в них одинаковое количество вопросов. Данная функция выравнивает количество вопросов в группах, добавляя скрытые вопросы, и перемешивает получившиеся группы. Функцию необходимо добавлять в глобальный скрипт Подготовка.

В неё необходимо передать массив с массивами номеров вопросов каждой группы: первый и последний номер, а если в группе один вопрос, то только его. Пример перемешивания пяти групп:

randomizeGroups([
[11001, 1100102],
[11002, 256],
[11003],
[11004],
[11005, 111]
]);

// -----------------------------------
function randomizeGroups(numbers) {
let allNums = questions.getNumbers();
let fakeNum = 888000;
let requaredNum = 0;
let firstNumbers = [];

for (let group of numbers) {
let qnStart = group[0];
let idxStart = allNums.indexOf(qnStart);
if (idxStart === -1) throw 'В анкете отсутствует вопрос Q' + qnStart;

firstNumbers.push(qnStart);

let qnEnd = group[1];
if (qnEnd === undefined) continue;

let idxEnd = allNums.indexOf(qnEnd);
if (idxEnd === -1) throw 'В анкете отсутствует вопрос Q' + qnEnd;

let num = idxEnd - idxStart;
if (num < 0) throw 'Группа для вопроса Q' + qnStart + ' выходит за границы списка вопросов';
if (num === 0) throw 'Вопрос Q' + qnStart + ' дважды указан в группе';
if (num > requaredNum) requaredNum = num;
}

for (let group of numbers) {
let qnStart = group[0];
let qnEnd = group[1];
if (qnEnd === undefined) {
addFakeQuestions(qnStart, requaredNum)

continue;
}

let idxStart = allNums.indexOf(qnStart);
let idxEnd = allNums.indexOf(qnEnd);
let num = idxEnd - idxStart;
if (num < requaredNum) addFakeQuestions(qnEnd, requaredNum)
}

questions.randomizeGroups(requaredNum + 1, firstNumbers);

function addFakeQuestions(qn, num) {
for (let i = 0; i < num; i++) {
let q = questions.insertAfter(qn, fakeNum++, 'fake', '', 'info');
q.condition = false;
}
}
}

Закрепить шапку всех табличных вопросов с выбором, чтобы не уезжала за пределы видимости

Заголовки таблиц теперь по умолчанию видны на экране.

Этот глобальный скрипт во время показа проверен в браузерах Chrome, Firefox, Opera, Internet Explorer 11, Edge и Vivaldi. В других или устаревших браузерах может не работать. При слишком большом масштабе страницы или при слишком маленьком размере окна браузера скрипт работает некорректно.

var qType = $('#ss_current_qtype').val();

if (qType === 'Table_SingleChoice' ||
qType === 'Table_MultipleChoice') {
var $maintable = $('table');

$maintable.append('<table id="header-fixed"></table>');

var $headerFixed = $('#header-fixed');

$headerFixed.css({
'position': 'fixed',
'top': '0px',
'display': 'none',
'background-color': 'white',
'border-bottom': '2px solid #ddd'
});

var tableOffset = $maintable.offset().top;
var $tableHeader = $maintable.children('thead');
var $fixedHeader = $headerFixed.append($tableHeader.clone());
var windowWidth = $(window).width();
var width = [];

getWidth();

$(window).bind('scroll', function() {
applyProperties();

var offset = $(this).scrollTop();

if (offset >= tableOffset && $fixedHeader.is(':hidden') && windowWidth > 767) {
$fixedHeader.show();
} else if (offset < tableOffset && !$fixedHeader.is(':hidden')) {
$fixedHeader.hide();
}
});

$(window).resize(function() {
getWidth();
applyProperties();
});

$('#surveybuttons > div > button').click(function() {
$fixedHeader.hide();
$(window).unbind('scroll');
});

function getWidth() {
windowWidth = $(window).width();

$.each($tableHeader.find('tr > th'), function(index, th) {
width[index] = $(th).width();
});
}

function applyProperties() {
$.each($fixedHeader.find('tr > th'), function(index) {
$(this).width(width[index]).css('padding', '5px');
});
}
}

Поле для фильтрации видимых ответов в текущем вопросе для браузера

Теперь у вопроса есть опция Добавить строку поиска.

Этот скрипт во время показа может не работать в устаревших браузерах. Чем длиннее список ответов, тем больше требуется ресурсов ПК (может работать медленно). В первой строке указываются коды ответов, которые должны всегда отображаться. Если таких кодов нет – оставьте пустые скобки.

var alwaysVisible = [98, 99];

var elAnswers = document.querySelector('div.ss-answers');

elAnswers.insertAdjacentHTML('beforebegin', '\
<div>\
<input id="search_box" class="form-control"\
style="margin-bottom: 12px; width: 300px;"\
type="text" autocomplete="off"\
placeholder="Поиск">\
</div>');

$('#search_box').focus(function () {
window.processing_isOpenValueFocused = true;
}).blur(function () {
window.processing_isOpenValueFocused = false;
});

var elAllRows = elAnswers.getElementsByTagName('tr');
var elSearchBox = document.getElementById('search_box');
var texts = [];

for (var i = 0; i < elAllRows.length; i++) {
var row = elAllRows[i];

row.style.backgroundColor = '#fff';

texts[i] = row.querySelector('span.summernote-html')
.textContent.trim();
}

var ms = 800; // для небольших списков задержку можно уменьшить
var typingTimer;

elSearchBox.addEventListener('keyup', function(k) {
switch(k.keyCode) {
// игнорирование нажатий
case 13: // enter
case 27: // escape
case 37: // стрелка влево
case 38: // стрелка вверх
case 39: // стрелка вправо
case 40: // стрелка вниз
return false;
}

clearTimeout(typingTimer);

var value = elSearchBox.value
.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');

if (!value.length) {
typingTimer = setTimeout(hideOthers, ms);
return false;
}

typingTimer = setTimeout(function() {
var expr = new RegExp(value, 'i');
var result = [];

for (var i = 0; i < texts.length; i++) {
if (!expr.test(texts[i])) result.push(i);
}

hideOthers(result);
}, ms);
});

function hideOthers(array) {
array = array === undefined ? [] : array;

for (var i = 0; i < elAllRows.length; i++) {
var elRow = elAllRows[i];
elRow.style.display = '';

var elInput = elRow.getElementsByTagName('input')[0];
var code = elInput.getAttribute('id').replace(/^.*c/, '');
var checked = elInput.checked;
var visible = alwaysVisible.indexOf(Number(code)) > -1;

if (checked || visible) continue;
if (array.indexOf(i) > -1) elRow.style.display = 'none';
}
}

Поле для фильтрации видимых ответов в текущем вопросе для браузера и приложения для планшета

Теперь у вопроса есть опция Добавить строку поиска.

В вопросе должен стоять флаг Проверка ответа скриптами. У первого по порядку ответа должен быть код 0 и флаги С открытым значением (текст), Всегда отображается, Исключить поле при выгрузке, Отключить выгрузку открытого значения, Не отображать код варианта ответа. Скрипт после ответа:

Q.showAll();

let checkedCodes = Q.getCheckedCodes();

if (checkedCodes.length == 0) return error('Требуется выбрать ответ');
if (!Q.isChecked(0)) return ok;

let value = Q[0].openValueTxt;

if (Q.isChecked(0) && value === undefined) {
return error('Уточните запрос или выберите ответ из списка');
}

Q.showOnly(checkedCodes);

value = value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');

let expr = new RegExp(value, 'i');
let result = [];

for (let a of Q.getAll()) {
if (expr.test(a.plainText)) result.push(a.code);
}

if (result.length == 0) return error('Ничего не найдено');

Q.show(result);

let msg = 'Выберите ответ среди найденных';

if (Q.type == QuestionTypeIds.MultipleChoice) {
msg += ' и снимите флаг с поля поиска';
}

msg += '. Или можно уточнить запрос';

return error(msg);

Ранжирование вариантов ответа вводом цифр

Добавлен новый тип вопроса — Ранжирование. Воспользуйтесь им.

В табличный числовой вопрос нужно добавить строки для ранжирования, поставить флаг Проверка ответа скриптами и добавить скрипт после ответа:

let qRows = Q.rows.getVisible();
let maxRank = qRows.length;
let uniqueAnswers = [];

for (let row of qRows) {
let rank = row.answer.openValueInt;

if (rank === undefined) {
return error('Необходимо ввести ответ в строке ' + row.code);
}

if (rank < 1 || rank > maxRank) {
return error('В строке ' + row.code +
' введено число меньше 1 или больше ' + maxRank);
}

if (uniqueAnswers.indexOf(rank) > -1) {
return error('В строке ' + row.code +
' повторяется число ' + rank);
}

uniqueAnswers.push(rank);
}

Если тип вопроса Таблица: единственный выбор, тогда скрипт должен быть такой:

let qRows = Q.rows.getVisible();
let maxRank = qRows.length;
let uniqueAnswers = [];

for (let row of qRows) {
let rank = row.getCheckedCode();

if (rank === undefined) {
return error('Необходимо выбрать ответ в строке ' + row.code);
}

if (rank < 1 || rank > maxRank) {
return error('В строке ' + row.code +
' выбрано число меньше 1 или больше ' + maxRank);
}

if (uniqueAnswers.indexOf(rank) > -1) {
return error('В строке ' + row.code +
' повторяется число ' + rank);
}

uniqueAnswers.push(rank);
}