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

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

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

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

Q1 - числовой вопрос. После него - единственный выбор со списком ответов 1-6 и скриптом перед показом:

let age = Q1.openValueInt;

Q[1].checked = age <= 17;
Q[2].checked = age >= 18 && age <= 24;
Q[3].checked = age >= 25 && age <= 34;
Q[4].checked = age >= 35 && age <= 44;
Q[5].checked = age >= 45 && age <= 54;
Q[6].checked = age >= 55;

return answered;

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

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;

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

Скрипт перед показом:

if (Q.visibleCount == 1) {
Q.getVisible()[0].checked = true;

return answered;
}

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

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

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;
}

Условие показа вопроса, находящегося внутри цикла

Q1 - вопрос, по ответам (1-98) которого задаются Q2 и Q3 с единственным выбором. Q3 должен выводиться на экран, только если в Q2 выбраны ответы 2 или 4. Суть простая: сначала получаем реальный вопрос Q2 для текущего ответа Q1, а потом работаем с ним как с обычным вопросом (без цикла). Глобальный скрипт перед показом:

if ((Q.number / 100 | 0) == 3) {
let Q2x = questions[200 + Q.number % 100];
if (!(Q2x.isChecked(2) || Q2x.isChecked(4))) return skip;
}

Ещё одно условие. Не задавать Q2 для ответа 3 в Q1:

if ((Q.number / 100 | 0) == 2) {
if (Q.number % 100 == 3) return skip;
}

Задать Q2 только для ответа 3 в Q1:

if ((Q.number / 100 | 0) == 2) {
if (Q.number % 100 != 3) return skip;
}

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

В переменной 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:
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;
}
}
}
}

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

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

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);
}

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

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"]').attr('disabled', true).parent().addClass('d-none');

В табличных:

$('input[id*="r500"]').attr('disabled', true).parent().addClass('d-none');
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);
}

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

Скрипт должен выполняться перед показом вопроса (глобально или в самом вопросе). Названия загруженных изображений должны содержать код ответа, к которому они относятся. В функцию addImages() опционально передаются часть названия изображения (если оно содержит не только код, например Logo_2) и расположение относительно текста.

/**
* Добавить к ответам/строкам Q8 картинки, названия которых содержат
* только код.
*/
if (Q.number == 8) {
addImages();
}

/**
* Добавить к ответам/строкам Q1, Q2, Q3 картинки, названия которых
* начинаются на «Марки_» (Марки_1, Марки_2, Марки_3…).
*/
if (Q.number >= 1 && Q.number <= 3) {
addImages('Марки_');
}

/**
* Добавить к ответам/строкам Q7 картинки, названия которых
* начинаются на «Логотипы_», и расположить их справа от текста.
*/
if (Q.number == 7) {
addImages('Логотипы_', ImagePlacementIds.Right);
}

// ––––––––––––––––––––––––––––––––––––––––––––––––––
function addImages(name, placement) {
let objName;

switch (Q.type) {
case QuestionTypeIds.SingleChoice:
case QuestionTypeIds.MultipleChoice:
objName = 'answers';
break;

case QuestionTypeIds.Table_Text:
case QuestionTypeIds.Table_Numeric:
case QuestionTypeIds.Table_SingleChoice:
case QuestionTypeIds.Table_MultipleChoice:
objName = 'rows';
break;
}

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

Этот скрипт во время показа может не работать в устаревших браузерах.

$('.ss-answers-clickable-img > img').css('width', '150px');

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

Скрипт перед показом для вопроса «Единственный выбор»:

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

let tag = contact.tag;
let name = 'Tag'; // имя поля, если отличается

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;

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

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

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;

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

В вопросе с единственным выбором должен быть полный список того, что может находиться в поле базы. Имя поля указывается в переменной name скрипта перед показом:

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:
tmp += oV === undefined ? ansDlmtr + '{1}' : ansDlmtr + '{1}' + oV;
break;

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

case QuestionTypeIds.Table_SingleChoice:
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:
tmp += oV === undefined ? '' : '_{1}' + oV;
break;

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

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

case QuestionTypeIds.Table_SingleChoice:
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 skip;
let id = interview.ipAddress + interview.userAgent;
if (isKeyLocked(id)) return exit();
V.key = id;
return skip;

После последнего вопроса добавьте ещё один информационный вопрос с номером, например, 998 и скриптом перед показом:

if (isPostProcessing() || isValidation()) return exit();
lockKey(V.key);
return exit();

Теперь во всех вопросах скринера, завершающих интервью если респондент не подходит, нужно сделать переход к Q998, вместо завершения. При успешном интервью респондент должен тоже попадать на Q998. Если в проекте квот нет, на этом можно остановиться. Но если они есть, необходимо в глобальный скрипт Обработка добавить:

if (isQuotaReached()) {
lockKey(V.key);
}

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

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

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

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 text = $nextBtn.text();

$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.text(text);
}
}, 1000);
}

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

if (isPostProcessing()) return ok;

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

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

f (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:
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($('#ss_current_qn').val());

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

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

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

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

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

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

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

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

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

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

Архив

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

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

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

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

// --------------------------------------------------------------
// 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);