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

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

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

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;

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

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

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

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

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

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 - вопрос, по ответам которого задаётся 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 нужно указать желаемое количество случайных ответов. Если требуется показать только определённые варианты ответа, то это необходимо делать сразу после первого if. Скрипт перед показом:

let num = 1;

if (Q.isAnswered) return answered;

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

let qCodes = shuffle(Q.getVisibleCodes());

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

    Q[code].checked = true;
}

return answered;

/* перемешать массив */
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;
}

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

Это расширенный аналог действия Перенести ответы из другого вопроса для вопросов с выбором. Коды ответов в обоих вопросах должны совпадать. Если код не найден - он будет пропущен. В функцию можно передать список кодов, которые переносить не нужно. Скрипт перед показом перенесёт в текущий вопрос ответы из 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;
            }
        }
    }
}

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

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

// --------------------------------------------------------------
// 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 – вопрос с множественным выбором. В нём выбираются ответы для дальнейшего ранжирования. После него нужно добавить служебный Q8001 с множественным выбором с последовательными кодами ответов (1,2,3... - по количеству ранжируемых ответов Q1) и скриптом перед показом:

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 div = 100;
let qN = Q.number / div | 0;
let code = Q.number % div;

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

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

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

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

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

questions.repeat(2, 2, 8001);

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

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

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;

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

В вопросе с единственным выбором должен быть полный список того, что может находиться в поле базы. Имя поля указывается в переменной 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);

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

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

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

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

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

/* перемешать массив */
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;

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

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

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

if (isExport()) {
    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;
}

Добавить имя переменной в текст вопроса

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

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 (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.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()) {
    let text = a.text.replace(/<\/?[^>]+>/g, '').trim();

    if (expr.test(text)) 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);

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

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

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

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

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) + ' сек.');

Комментарии