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

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

Автоматическое кодирование возраста.
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 <= 30;
Q[4].checked = age >= 31 && age <= 45;
Q[5].checked = age >= 46 && age <= 64;
Q[6].checked = age >= 65;

return answered;
Вывести на экран имя респондента.
В текстовый вопрос добавить скрипт после ответа:
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[Q.getVisibleCodes()[0]].checked = true;

    return answered;
}
Сделать в табличном вопросе с выбором необязательной для ответа строку с текстовым полем.
В свойствах вопроса необходимо поставить флаг Не требовать обязательного ответа на вопрос (проверка ответа скриптами) и добавить скрипт после ответа:
for (let row of Q.rows.getVisible()) {
    let codes = row.getCheckedCodes();

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

        continue;
    }

    if (codes.length == 0) {
        return error('Необходимо выбрать ответ в строке ' + row.code);
    }
}
Если текстовое поле должно быть обязательным для заполнения - перед continue; необходимо добавить:
if (!row.openValueTxt && codes.length > 0) {
    return error('Необходимо заполнить открытое поле в строке ' + row.code);
}
Выводить текст из «другого» вместо {answerText} в вопросах внутри цикла.
Так как макрос {answerText} заменяется на текст ответа при запуске анкеты и поэтому не подходит для вывода текста из полей, в тексте вопроса нужно использовать глобальную переменную (имя - на своё усмотрение). Здесь используется {Магазин}. Q1 - вопрос, по ответам которого задаётся Q2. Глобальный скрипт перед показом:
let qN = Q.number / 100 | 0;
let code = Q.number % 100;

if (qN == 2) {
    let a = Q1[code];

    V['Магазин'] = a.flags & AnswerFlags.OpenValueTxt ? a.openValueTxt : a.text;
}
Выбрать случайный вариант ответа.
В переменной NUM нужно указать желаемое количество случайных ответов. Скрипт перед показом:
let num = 1;

if (Q.isAnswered) return answered;

Q.answers.randomize();

let qCodes = Q.getCodes();

for (let i = 0; i < num; i++) {
    let code = qCodes[i];

    Q[code].checked = true;
}

return answered;
Перенести ответы из одного вопроса с выбором в другой.
Коды ответов в обоих вопросах должны совпадать. Если код не найден - он будет пропущен. В функцию можно передать список кодов, которые переносить не нужно. Скрипт перед показом перенесёт в текущий вопрос ответы из 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;
        
        Q[a.code].checked = true;
        Q[a.code].openValueNum = a.openValueNum;
        Q[a.code].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;
Показывать список кодов в зависимости от выбранного в предыдущем вопросе ответа.
Этот скрипт перед показом удобно использовать для вывода большого списка кодов с неупорядоченной нумерацией. В функцию можно передавать как один код, так и массив кодов. Автоматически определяет тип текущего вопроса. Если в табличном вопросе нужно выводить не строки, а столбцы - нужно раскомментировать строку //objName = 'columns';.
let codes = {};

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

showOnlyCodesByCode(Q1.getCheckedCode());

return Q.hasVisible ? ok : skip;

// ----------------------------------------
function showOnlyCodesByCode(codeOrArray, silentIfNoCodeFound) {
    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';
            //objName = 'columns';
            break;
            
        default:
            printError('Данный тип вопроса не поддреживается ' +
                       'функцией showOnlyCodesByCode()');
            return false;
    }
    
    Q[objName].hideAll();
    
    switch (typeof codeOrArray) {
        case 'number':
            show(codeOrArray);
            break;

        case 'object':
            for (let code of codeOrArray) {
                show(code);
            }
            break;
            
        default:
            printError('В функцию showOnlyCodesByCode() передано ' +
                       'недопустимое значение: ' + codeOrArray);
            return false;
    }
    
    function show(code) {
        if (code in codes) {
            let currentArray = codes[code];
        } else {
            if (!silentIfNoCodeFound) {
                printError('В объекте codes не найден код ' + code);
            }
            return false;
        }
        
        for (let elem of currentArray) {
            switch (typeof elem) {
                case 'number':
                    Q[objName].show(currentArray[i]);
                    continue;

                case 'object':
                    Q[objName].showFromTo(elem[0], elem[1]);
                    break;
            }
        }
    }
    
    function printError(comment) {
        let errorText = 'Произошла ошибка! ';
        
        informationTextAdd(errorText + comment);
    }   
}
Замер времени ответов на вопросы.
Глобальный скрипт Подготовка (если есть циклы по вопросам - вставлять в конец скрипта):
// --------------------------------------------------------------
// 0 - не добавлять время в массив, 1 - добавлять
let enableExport = 1;
// номер вопроса, в котором будем хранить время
V.QTn = 12345678;
// номер первого вопроса анкеты, перед которым будет время ответов
let FQn = questions.getNumbers()[0];
// вставляем вопрос, в котором будем хранить время ответов
let QT = questions.insert(FQn, V.QTn, 'Замер времени', '', '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 == V.QTn) 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[V.QTn];
    // для всех вопросов, кроме того, в котором храним время
    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;
        }
    }
}
// --------------------------------------------------------------
Закрепить шапку таблицы, чтобы не уезжала за пределы видимости.
Этот скрипт во время показа проверен в браузерах Chrome, Firefox, Opera, Internet Explorer 11, Edge и Vivaldi. В других или устаревших браузерах может не работать. При слишком большом масштабе страницы или при слишком маленьком размере окна браузера скрипт работает некорректно.
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');
    });
}
Ранжирование вариантов ответа вводом цифр.
В табличный числовой вопрос нужно добавить строки для ранжирования, поставить флаг Не требовать обязательного ответа на вопрос (проверка ответа скриптами) и добавить скрипт после ответа:
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);
}
Выводить указанное в предыдущем вопросе количество строк таблицы.
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"]').remove();
В табличных вопросах удаляются input и коды строк, которые содержат значение из переменной code (1500, 500, 50010):
var code = 500;
var $sections = $('tr:contains(' + code + ')');

$sections.each(function() {
    var tds = $('td', $(this));

    tds.eq(0).attr('colspan', tds.length);
    tds.not(':eq(0)').remove();
    
    var withoutCode = tds.html().replace(/>\d+./, '>');
    
    tds.html(withoutCode);
});
Выбор варианта ответа с наименьшим значением счётчика.
Скрипт перед показом проходит по всем ответам текущего вопроса и ищет счётчики с названиями вариантов ответа. Затем выбирает ответ с наименьшим значением счётчика. Если в счётчике указано значение квоты и она закрыта - счётчик пропускается. Скрипт может работать с любым количеством ответов.
if (Q.isAnswered) return answered;

let codes = [];

for (let a of Q.getVisible()) {
    let counterName = a.text.replace(/<\/?[^>]+>|&.*?;/g, '').trim();
    let counter = getCounter(counterName);

    if (counter === undefined) {
        Q.comment = 'Произошла ошибка! Не найден счётчик «' + counterName + '»';
        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 exit('Квоты в проверяемых счётчиках закрыты.');

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

Q[codes[0].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 sum = 100;
let total = 0;

for (let row of Q.rows.getVisible()) {
    let num = row.answer.openValueNum;
    
    total += num ? num : 0;
}

if (total != sum) {
    return error('Сумма значений не равна ' + sum + '. Сейчас ' + total);
}
Добавить изображения к вариантам ответа или строкам таблицы.
Скрипт добавляется в Подготовку. Названия загруженных изображений должны содержать код ответа, к которому они относятся. В функцию addImages() передаются номер вопроса и часть названия изображений, если она есть.
/*
 Добавить к ответам/строкам Q8 картинки, названия которых содержат 
 только код.
*/
addImages(8);

/*
 Добавить к ответам/строкам Q1, Q2, Q3 картинки, названия которых 
 начинаются на «Марки_» (Марки_1, Марки_2, Марки_3...).
*/
[1,2,3].forEach(function(n) {
    addImages(n, 'Марки_');
});

/*
 Добавить к ответам/строкам Q7 и Q9 картинки, названия которых 
 начинаются на «Логотипы_» (Логотипы_1, Логотипы_2, Логотипы_3...).
*/
[7,9].forEach(function(n) {
    addImages(n, 'Логотипы_');
});

function addImages(n, name) {
    let qN = questions[n];
    let objName;
    
    if (name === undefined) name = '';

    switch (qN.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;
    }
    
    for (let a of qN[objName].getAll()) {
        a.image = images[name + a.code];
    }
}
Установить всем картинкам у вариантов ответа ширину 150px.
Этот скрипт во время показа может не работать в устаревших браузерах.
$('.ss-answers-clickable-img > img').css('width', '150px');
Подстановка ответа (числового кода) из метки базы контактов или из поля «Tag».
Скрипт перед показом для вопроса «Единственный выбор»:
if (isTesting()) return ok;
if (isPostProcessing() || isValidation()) return answered;

let tag = contact.tag;

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

if (tag === undefined) {
    informationTextAdd('ВНИМАНИЕ! Не найдена метка ни в свойствах базы ' +
                       'контактов, ни в её поле «Tag».');
    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;
Завершить интервью, если выбран только ответ 3, но продолжить, если он выбран вместе с другим ответом.
Это можно сделать простым действием Завершить интервью после ответа с обычным выражением в условии. Но можно и скриптом после ответа:
if (Q.getCheckedCodes().length == 1 && Q.isChecked(3)) {
    return exit();
}
Перемешать варианты ответа блоками.
Q1 - вопрос с выбором, в котором нужно перемешать ответы блоками. Коды ответов, входящих в блоки, указываются в первой строке. Скрипт Подготовка:
let blocks = [[5001,1,2,3], [5002,4,5,6], [5003,7,8,9]];

Q1.answers.setOrder(shuffleBlocks(blocks));

function shuffleBlocks(array) {
    let result = [];

    for (let codes of shuffle(array)) {
        result = result.concat(codes);
    }
    
    return result;
}

/* перемешать массив */
function shuffle(numPool) {
    for (
        let j, x, i = numPool.length; i; 
        j = parseInt(Math.random() * i, 10), 
        x = numPool[--i], 
        numPool[i] = numPool[j], 
        numPool[j] = x
        );

    return numPool;
}
Показать ответы или строки, для которых в строках табличного вопроса выбраны ответы с кодом 1.
Q1 - табличный вопрос с выбором. В следующем вопросе должен быть такой же список ответов или строк и скрипт перед показом:
// можно менять на 'answers' или 'columns' в зависимости от типа текущего вопроса
let objName = 'rows';

Q[objName].hideAll();

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

return Q[objName].visibleCount > 0 ? ok : skip;
Автоматически прописать имена переменных.
В вопросах анкеты нужно указать желаемые имена без подстановок {1}, {2}, но соблюдая требования, описанные в начале этой статьи. Если вопрос находится внутри цикла, вместе с именем должна быть указана подстановка {3}. Скрипт добавляет к указанному имени переменной подстановки, в соответствии с типом вопроса, а также прописывает имена для числовых и текстовых значений. Скрипт Подготовка должен начинаться так:
for (let q of questions.getAll()) {
    let tmp = q.outputColumnTemplate;
    
    if (tmp === undefined || tmp[0] == '{') continue;
    
    if (q.flags & QuestionFlags.SkipExport) {
        q.outputColumnTemplate = undefined;
        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) {
    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;
}
Этот скрипт можно модифицировать для автоматического вывода имён переменных в текстах вопросов. Для этого после первого if нужно добавить:
let qName = tmp.replace(/\{3\}/, '{answerCode}');
q.text = '<p><font color="gray">' + qName + '</font></p>' + 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);
}
Не сохранять ответ с флагом «Исключить поле при выгрузке».
Скрипт после ответа:
for (let a of Q.getChecked()) {
    if (a.flags & AnswerFlags.SkipExport) a.checked = false;
}
Запретить повторное заполнение анкеты в веб-опросе.
При анонимном опросе идентифицировать респондента на 100% невозможно, поэтому у него всегда останется возможность обойти ограничение. Блокировать анонимного пользователя можно по его IP-адресу и браузеру. Добавьте в начало анкеты информационный вопрос с любым текстом (он не будет отображаться на экране) и со скриптом перед показом:
if (isTesting() || isPostProcessing() || isValidation()) return skip;
let id = respondent.ipAddress + respondent.userAgent;
if (isKeyLocked(id)) return exit();
V.key = id;
return skip;
После последнего вопроса добавьте ещё один информационный вопрос с номером, например, 998 и скриптом перед показом:
if (isTesting() || isPostProcessing() || isValidation()) return exit();
lockKey(V.key);
return exit();
Теперь во всех вопросах скринера, завершающих интервью если респондент не подходит, нужно сделать переход к Q998, вместо завершения. При успешном интервью респондент должен тоже попадать на Q998. Если в проекте квот нет, на этом можно остановиться. Но если они есть, необходимо в глобальный скрипт Обработка добавить:
if (isQuotaReached()) {
    lockKey(V.key);
}
Так как по умолчанию интервью, превысившее квоту, не сохраняется в базу данных, скрипт Обработка выполняться не будет – блокировка не сработает. Поэтому нужно включить сохранение таких интервью.
Поле для фильтрации видимых ответов в текущем вопросе для браузера.
Этот скрипт во время показа может не работать в устаревших браузерах. Чем длиннее список ответов, тем больше требуется ресурсов ПК (может работать медленно). В первой строке указываются коды ответов, которые должны всегда отображаться. Если таких кодов нет – оставьте пустые скобки.
var alwaysVisible = [98, 99];

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

elAnswers.insertAdjacentHTML('beforebegin', '\
<div class="row col-md-12">\
    <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.includes(Number(code));
        
        if (checked || visible) continue;
        if (array.includes(i)) 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.showOnlyCodes(checkedCodes);

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

let expr = new RegExp(value, 'i');
let result = [];
        
for (let a of Q.getAll()) {
    let text = a.text.replace(/<\/?[^>]+>/g, '').trim();
    
    if (expr.test(text)) result.push(a.code);
}

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

Q.showCodes(result);

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

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

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

return error(msg);
Отключить кнопку «Далее» на определённое время, если в вопросе нет ответа.
Этот скрипт во время показа может не работать в устаревших браузерах.
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');

    $nextBtn.hide();
    $nextBtn.attr('disabled', true);
 
    setTimeout(function() {
        $nextBtn.attr('disabled', false);
        $nextBtn.show();
    }, time * 1000);
}
Кодирование текста из поля базы контактов.
В вопросе с единственным выбором должен быть полный список того, что может находиться в поле базы. Имя поля указывается в переменной 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()) {
    let plainText = a.text.replace(/<\/?[^>]+>/g, '').trim();

    if (plainText.toUpperCase() == value.trim().toUpperCase()) {
        a.checked = true;
        return answered;
    }
}

informationTextAdd('ВНИМАНИЕ! Отсутствует ответ для текста «{0}», \
который указан в поле «{1}» базы контактов.', value, name);
Ранжирование вариантов ответа вопросами.
Q1 – вопрос с множественным выбором. В нём выбираются ответы для дальнейшего ранжирования. Коды ответов должны быть последовательные (1,2,3...). После него нужно добавить служебный Q8001 с множественным выбором, такими же кодами и скриптом перед показом:
Q.reset();

let num = Q1.getCheckedCodes().length;

for (let i = 1; i <= num; i++) {
    Q[i].checked = true;
}

return Q.isAnswered ? answered : skip;
Далее нужно добавить Q2 с единственным выбором и теми же ответами для ранжирования. В тексте вопроса можно использовать подстановку {answerCode}, например «{answerCode} место:». В глобальный скрипт перед показом нужно добавить:
let divisor = 100;
let qN = Q.number / divisor | 0;
let code = Q.number % divisor;

if (qN == 2) {
    Q.showOnlyCodes(Q1.getCheckedCodes());

    for (let a of Q8001.getCheckedCodes()) {
        if (a == code) break;

        Q.hide(questions[qN * divisor + a].getCheckedCode());
    }

    if (Q.visibleCount == 1) {
        Q[Q.getVisibleCodes()[0]].checked = true;
        return answered;
    }
}
Если максимальный код ответа в Q8001 недвузначный, в переменной divisor нужно изменить делитель. Также в Подготовку нужно добавить:
questions.repeat(2, 2, 8001);