Расширение шаблона на Javascript

Задание представляет собой набор классов (Javascript-функций), которые реализуют все аспекты его жизненного цикла. Вы можете использовать классы задания для создания нестандартных шаблонов.

Список классов:

  • Task.

    Отвечает за задание, его отрисовку, валидацию и интерфейс взаимодействия. Если от задания требуется нестандартное поведение, как правило нужно расширить и переопределить методы Task.

  • TaskSuite.

    Класс-«обертка» для страницы заданий, создает экземпляры классов заданий. Вы можете переопределить этот класс, например, чтобы отобразить общий элемент на странице с заданиями или получить больший контроль над заданиями (нестандартные горячие клавиши или т.п.).

Для решения специфических задач (например, подписка на нажатие клавиш, получение GPS-координат пользователя) можно использовать сервисы.

Жизненный цикл задания

Когда пользователь начинает выполнять задания, во встроенном фрейме инициализируется его рабочее место. Выполняется запрос на получение страницы заданий. Для отображения страницы создается экземпляр класса TaskSuite, который создает экземпляр класса Task для каждого задания.

Для отрисовки страницы заданий (TaskSuite) вызывается метод #render(), который вызывает метод для каждого объекта задания, добавляет их элементы в свой элемент DOM.

После выполнения или пропуска заданий страница уничтожается: вызывается метод #destroy() для страницы заданий, а затем каждого задания. После этого запрашивается новое задание и процесс повторяется.

Для удобства использования базовых классов в TaskSuite и Task определены методы #onRender() и #onDestroy(). Метод #onRender() можно переопределить, чтобы изменить DOM-элемент, созданный методом #render(). Метод #onDestroy() пригодится, чтобы очистить ресурсы (зарезервированные в глобальном пространстве обработчики, кэши и т. п.) после выполнения задания. Такая очистка может понадобиться, чтобы не засорять память и не провоцировать утечки данных.

Наследование классов

Чтобы код не выглядел громоздким, для наследования и переопределения классов можно использовать функцию:

function extend(ParentClass, constructorFunction, prototypeHash) {    
    constructorFunction = constructorFunction || function() {};    
    prototypeHash = prototypeHash || {};        
    if (ParentClass) {        
        constructorFunction.prototype = Object.create(ParentClass.prototype);    
    }    
    for (var i in prototypeHash) {        
        constructorFunction.prototype[i] = prototypeHash[i];    
    }    
    return constructorFunction;
}

Вызов функции:

var ChildClass = extend(ParentClass, function() {    
    ParentClass.call(this);
}, {    
    method: function() {}
})

Класс Task

Базовый класс Task отвечает за интерфейс задания. Класс доступен в глобальной переменной window.TolokaTask.

Методы Task
class TolokaTask {
    
    /**
     * Конструктор интерфейса задания.
     * @param {Task} options.task Модель задания.
     * @param {Specs} options.specs Спецификации (входные и выходные данные, шаблон).
     * @param {{isReadOnly: boolean}} [options.workspaceOptions] Параметры инициализации песочницы.
     */
    constructor(options) {...}
    
    /**
     * @return {Object} Параметры, переданные конструктору (см. формат options конструктора).
     */
    getOptions() {}
    
    /**
     * @return {Object} Параметры инициализации песочницы (см. формат options.workspaceOptions конструктора).
     */
    getWorkspaceOptions() {}
    
    /**
     * @return {Task} Модель задания.
     */
    getTask() {}
    
    /**
     * @return {Solution} Ответы задания.
     */
    getSolution() {}
    
    /**
     * Полный URL для доступа к данным на прокси-сервере.
     * @param {string} path относительный URL файла>
     * @return {string} полный URL 
     */
    getProxyUrl() {}
    
    /**
     * @param {Solution} solution Устанавливаемые ответы.
     */
    setSolution(solution) {}

    /**
     * @return {HTMLElement} DOM-элемент задания.
     */
    getDOMElement() {}

    /**
     * @return {HTMLElement} DOM-элемент для стилей задания, который добавляется в document.head при отрисовке.
     */
    getStyleDOMElement() {}

    /**
     * Валидирует ответы согласно параметрам входных данных.
     * @param {Solution} [solution] Решение задания. Если не передается, берется текущее (#getSolution()).
     * @return {SolutionValidationError|null} Если ответы корректные, возвращает null.
     */
    validate(solution) {}

    /**
     * Реализует логику установки фокуса на задание, вызывает метод #onFocus().
     */
    focus() {}

    /**
     * Реализует логику снятия фокуса с задания, вызывает метод #onBlur().
     */
    blur() {}

    /**
     * Шаблонизатор задания. Берет поле markup из шаблона и заменяет вхождения типа ${fieldX} на соответсвующее значение с ключем fieldX из параметра data.
     * @param {Object} data Объект с данными для подстановки в шаблон.
     * @return {string} HTML строка.
     */
    template(data) {}

    /**
     * Формирует DOM-представление интерфейса задания. Вызывает #onRender().
     * @return this
     */
    render() {}

    /**
     * Освобождает занятые в глобальном пространстве ресурсы, сервисы, обработчики событий. Вызывает #onDestroy().
     */
    destroy() {}

    /**
     * Вызывается после отрисовки задания (#render()). Все манипуляции с DOM-элементом задания следует производить здесь.
     */
    onRender() {}
    /**
     * Вызывается после уничтожения задания (#destroy()). Наиболее подходящий метод для очистки занятой памяти, удаления глобальных обработчиков событий, DOM-элементов и т. п.
     */
    onDestroy() {}

    /**
     * Вызывается после установки фокуса.
     */
    onFocus() {}

    /**
     * Вызывается после удаления фокуса.
     */
    onBlur() {}

    /**
     * @param {string} key Буквенно-числовой символ, нажатый на клавиатуре. Может быть использован как горячая клавиша.
     */
    onKey(key) {}

    /**
     * Вызывается после неудачной валидации с описанием ошибки в параметре. 
     * @param {SolutionValidationError} errors
     */
    onValidationFail(errors) {}
}

Класс TaskSuite

Основная задача класса TaskSuite — отрисовать задания на странице (метод #render()). Через него также происходит сбор ответов (#getSolutions()), валидация (#validate(solutions)) и управление горячими клавишами (#focusNextTask(), #onKey(key) и т.д.).

Базовый класс для TaskSuite доступен в глобальной переменной window.TolokaTaskSuite.

Методы TaskSuite
class TolokaTaskSuite {

    /**
     * Констуктор базового класса страницы заданий.
     * 
     * @param {Task[]} options.tasks Массив моделей заданий.
     * @param {Specs} options.specs Спецификации (входные и выходные данные, шаблон).
     * @param {String} options.assignmentId Идентификаторы выданных работнику заданий.
     * @param {{ isReadOnly: boolean }} [options.workspaceOptions] Параметры инициализации песочницы.
     * @param {Function} [options.TaskClass=BaseTask] Класс создаваемых заданий.
     * @param {Solution[]} [options.solutions] Массив ответов (если есть).
     */
    constructor(options) {}

    /**
     * @return {Object} Параметры, переданные конструктору (см. формат options конструктора)
     */
    getOptions() {}

    /**
     * @return {Object} Параметры инициализации песочницы (см формат options.workspaceOptions конструктора).
     */
    getWorkspaceOptions() {}

    /**
     * @return {Task[]} Массив иницализированных (согласно переданным моделям) заданий.
     */
    getTasks() {}

    /**
     * Метод для более удобного обращения к заданию по его идентификатору.
     * 
     * @return  {{"<taskId>": Task, ... }} Индексированные по идентификаторам задания.
     */
    getTasksIndexed() {}

    /**
     * Полный URL для доступа к данным на прокси-сервере.
     * @param {{<относительный URL файла>}}
     * @return  {{<полный URL>}} 
     */
    getProxyUrl() {}

    /**
     * Собирает ответы с заданий.
     *
     * @return {Solution[]} Массив ответов.
     */
    getSolutions() {}

    /**
     * @return {HTMLElement} DOM-элемент страницы (до отрисовки пустой, после - инициализированный, содержащий интерфейс).
     */
    getDOMElement() {}

    /**
     * @return {HTMLElement} DOM-элемент стилей страницы.
     */
    getStyleDOMElement() {}

    /**
     * Валидирует ответы по параметрам выходных данных.
     * 
     * @param {Solution[]} [solutions] Массив решений. Если не передается,  берутся текищие через #geSolutions().
     * @return {Promise | SolutionValidationError[] | null} Либо промис (с массивом ошибок или null), либо массив ошибок, либо null, если все ответы валидны. 
     */
    validate(solutions) {}

    /**
     * Инициализатор обработчиков горячих клавиш:
     *   - Устанавливает фокус на предыдущее задание по стрелке влево/вверх.
     *   - Устанавливает фокус на следующее задание по стрелке вправо/вниз.
     *   - Передает нажимаемые клавиши активному заданию.
     *   - Устанавливает фокус на первое задание.
     */
    initHotkeys() {}

    /**
     * Устанавливает фокус на задание по индексу.
     * 
     * @param {string} index Индекс задания в массиве.
     */
    focusTask(index) {}

    /**
     * Устанавливает фокус на следующее задание.
     */
    focusNextTask() {}

    /**
     * Устанавливает фокус на предыдущее задание.
     */
    focusPreviousTask() {}

    /**
     * Передает нажатую клавишу активному заданию.
     */
    onKey(key) {}

    /**
     * Формирует DOM-представление страницы заданий: отрисовывает все задания на странице (вызывает #render()). Вызывает #onRender().
     *
     * @return this
     */
    render() {}

    /**
     * Уничтожает все  задания старницы. Освобождает занятые в глобальном пространстве ресурсы, сервисы, обработчики событий. Вызывает #onDestroy().
     */
    destroy() {}

    /**
     * Вызывается после отрисовки страницы (#render()). Все манипуляции с DOM-элементом следует производить здесь.
     */
    onRender() {}

    /**
     * Вызывается после уничтожения страницы (#destroy()). Наиболее подходящее место для очистки занятой памяти, удаления глобальных обработчиков событий, DOM-элементов и т. п.
     */
    onDestroy() {}

    /**
     * Вызывается после неудачной валидации с описанием ошибок в параметре. 
     *
     * @param {SolutionValidationError[]} errors Массив ошибок.
     */
    onValidationFail(errors) {}
}

Типы данных

Объект Task

{
    "id": <строка>,
    "input_values": {
        "<id поля с входными данными>": <значение>,
        …
     }
}

Ключ

Значение

id

Идентификатор задания.

input_values

Входные данные задания в формате "<id поля>":"<значение поля>". Пример:
"input_values": {
            "image": "http://images.com/1.png"
          }

Объект Solution

{
    "task_id": <строка>,
    "output_values": { 
        "<id поля ввода>": <значение>,
        …
    }
}

Ключ

Значение

task_id

Идентификатор задания.

output_values

Ответы в формате "<id поля ввода>":"<значение>". Пример:
"outputValues": {
            "colour": "white",
            "comment": "So white"
          }

Объект SolutionValidationError

{
    "task_id": string,
    "errors": { 
        "<id поля ввода>": {
            "code": "<код ошибки>",
            ["message": <строка>]
        },
        …
    }
}

Ключ

Значение

task_id

Идентификатор задания.

errors

Ошибки в формате: "<id поля ввода>": {code: "<код ошибки>", [message: "<сообщение об ошибке>"]}. Пример:
"errors": { 
        "colour": {
            "code": "REQUIRED",
            ["message": "Обязательное поле"]
        },

Сервисы

Для решения специфических задач в базовых классах доступны сервисы:

  • Geolocation.

    Позволяет получить GPS-координаты пользователя, если они доступны. В TaskSuite и Task доступен через this.geolocation.

    class Geolocation {
    
       /**
        * Повторяет функциональность navigator.geolocation.getCurrentPosition() - https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition
        */
       getCurrentPosition(success, error, options) {}
    
       /**
        * Повторяет функциональность navigator.geolocation.watchPosition() - https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition
        */
       watchPosition(success, error, options) {}
    }
    
  • Storage

    Сохранение данных на клиенте. В TaskSuite и Task доступен через this.storage.

    class Storage {
    
       /**
        * Сохранить значение под определенным ключем.
        *
        * @param {string} key — ключ.
        * @param {*} value — значение. Может быть любого типа, сериализуется приведением к строке.
        * @param {Date|number} [expiration] — дата истечения срока хранения. По умолчанию 24 часа.
        */
       setItem(key, value, expiration) {}
    
       /**
        * Получить сохраненное значение по ключу.
        *
        * @param key — ключ.
        * @returns {*}
        */
       getItem(key) {}
    
       /**
        * Удалить значение по ключу.
        *
        * @param {string} key
        */
       removeItem(key) {}
    }
    
  • TaskInterface

    Интерфейс задания. В TaskSuite и Task доступен через this.taskInterface.

    class TaskInterface {
    
        /*
         * Открывает инструкцию.
         */
         showInstructionPopup() {}
    
       /**
         * Раскрывает задание на весь экран, если оно свернуто. Иначе свернет задание.
         */
         toggleFullscreen() {}
    }
  • Hotkey

    Позволяет подписаться на нажатие клавиш. В TaskSuite и Task доступен через this.hotkey.

    class Hotkey {
       /**
        * Подписывает переданный обработчик на определенное событие. Отслеживаются следующие события (параметр event):
        *  "enter" — клавиша «ввод».
        *  "esc" — клавиша «отмена».
        *  "arrow-left", "arrow-right", "arrow-up", "arrow-down" — стрелки.
        *  "key" — буквенно-числовые клавиши. Обработчик (параметр handler) получит нажатую клавишу в качестве первого аргумента.
        * 
        * @param {string} event — имя события
        * @param {Function} handler — обработчик события
        * @param {Object} context — "this" для обраточика
        */
       on(event, handler, context) {}
    }