Assessment-блоки
Имена/команды: skin —
wp2lms-turbo-blocks-mos(npm run buildв каталоге пакета). Устаревшиеbuild:all/wp2lms-turbo-pulse— см. обзор Turbo.
Как добавлять новые типы вопросов wp2turbo/* (например «Сортировка», «Сопоставление»), чтобы они:
- Работали standalone на странице (как сейчас choice / open).
- Встраивались в
wp2turbo/question-bank(нумерация, «Далее», прогресс). - Писали прогресс в ту же LMS-схему (
questions/quiz), что Elementor / wp2lms-pro.
Эталонные реализации:
| Тип | Блок | Alpine | Twig | Transformer |
|---|---|---|---|---|
| Выбор | blocks/choice-question |
question-choice.js |
QuestionChoice.twig |
Wp2TurboChoiceQuestionTransformer |
| Открытый | blocks/open-question |
question-open.js |
QuestionOpen.twig |
Wp2TurboOpenQuestionTransformer |
| Банк (оркестратор) | blocks/question-bank + question-bank-questions |
question-bank.js |
QuestionBank.twig |
Wp2TurboQuestionBank* |
Общий гайд по wp2turbo-блокам (tabs / icon-list): AI_TURBO_BLOCKS.md.
Контракт LMS store: LMS_QUIZ_STORAGE.md.
Shared kit (tech debt reduction)
Заголовок раздела «Shared kit (tech debt reduction)»| Что | Путь |
|---|---|
| Манифест типов (allowlist + event prefix) | blocks/_shared/question-types/registry.js |
| Sync factory (секции prompt/feedback/items) | blocks/_shared/question-sync/create-question-sync.js |
| Банк слушает события из registry | assets/src/frontend/question-bank.js |
| Status transformer (correct/incorrect/partial) | Wp2TurboQuestionStatusTransformer (+ alias Wp2TurboOpenQuestionStatusTransformer) |
| Choice partial feedback | blocks/choice-question-partial — только при selectionType=multiple + showFeedback; outcome partial (points=0) |
Новый тип: сначала запись в registry.js, затем блок/Alpine/Twig; listeners банка и visibility подхватятся из манифеста.
Предварительные условия
Заголовок раздела «Предварительные условия»- Активны WP2LMS, WP2LMS Pro, Turbo, Blocks.
- Сборка:
npm run build:all(илиbuild:skinпосле правок Alpine/блоков Blocks). - Не править
wp-content/plugins/**вручную — только исходники + build.
Когда этого документа достаточно
Заголовок раздела «Когда этого документа достаточно»| Задача | Достаточно? |
|---|---|
| Новый тип вопроса + совместимость с банком и LMS | Да |
| Только UI/стили существующего choice/open | Нет — править блок/CSS; банк трогать не нужно |
| Новый обычный wp2turbo-блок без проверки знаний | Нет — AI_TURBO_BLOCKS.md |
Менять контракт quiz / page.progress |
Нет — LMS_QUIZ_STORAGE.md + согласование с враппером |
Архитектура: вопрос ↔ банк ↔ LMS
Заголовок раздела «Архитектура: вопрос ↔ банк ↔ LMS»flowchart TB subgraph editor [Gutenberg] QB[question-bank-questions] Q[wp2turbo/sort-question] QB -->|InnerBlocks + factory| Q end
subgraph front [Turbo front] TwigQ[QuestionSort.twig] AlpineQ[turboQuestionSort] TwigB[QuestionBank.twig] AlpineB[turboQuestionBank] TwigQ --> AlpineQ TwigB --> AlpineB AlpineQ -->|"*:answered / *:next"| AlpineB end
subgraph lms [wp2lms-pro] QS["questions store"] Quiz["quiz store"] AlpineQ -->|writeQuestion| QS AlpineB -->|addQuizScore / patchQuiz| Quiz end
editor --> frontПравило баллов: один вопрос в банке = до 1 балла (points 0|1). Банк суммирует через addQuizScore({ scorePoints: 0|1 }).
Shared assessment kit (Phase 3a)
Заголовок раздела «Shared assessment kit (Phase 3a)»| Что | Путь | Назначение |
|---|---|---|
| Реестр типов | blocks/_shared/question-types/registry.js |
blockName + eventPrefix (+ component); allowlist банка и Alpine-слушатели *:answered / *:next |
| Sync factory | blocks/_shared/question-sync/create-question-sync.js |
createQuestionSync — секции prompt/body/feedback, child locks; используют sort/match/classify/choice *-sync.js |
| Prompt HTML (PHP) | ResolvesQuestionPromptHtmlTrait |
Общий разбор HTML prompt в transformers |
| Status (PHP) | Wp2TurboQuestionStatusTransformer |
correct/incorrect иконки; BC-alias: Wp2TurboOpenQuestionStatusTransformer |
Новый тип: сначала запись в registry, затем sync через createQuestionSync (или тонкая обёртка), слушатели банка подтянутся из QUESTION_EVENT_PREFIXES.
Контракт событий (обязателен для банка)
Заголовок раздела «Контракт событий (обязателен для банка)»Банк слушает на корне turboQuestionBank кастомные события (bubbling). Префиксы берутся из QUESTION_EVENT_PREFIXES — новый тип достаточно добавить в registry (плюс слать события с тем же eventPrefix).
| Событие | Когда | detail (минимум) |
|---|---|---|
{type}-question:answered |
После проверки ответа | wp2Id, isCorrect (bool), hasFeedback (bool) |
{type}-question:next |
Клик «Далее» в банке | wp2Id, isCorrect |
Эталон имён: choice-question:*, open-question:*. Для сортировки, например: sort-question:answered / sort-question:next.
Доп. поля в detail (по типу):
- Choice:
selected,selectedOptionWp2Ids - Open:
answer,answerType - Sort (пример):
order(массив id элементов) — банк их не требует, но полезны для отладки /resultPayload
Если hasFeedback === false, банк может сразу вызвать goNext() (как у choice без feedback).
UI в банке vs standalone
Заголовок раздела «UI в банке vs standalone»| Режим | inQuestionBank |
После ответа |
|---|---|---|
| Standalone | false |
Кнопка «Повторить» (reset()) |
| В банке | true |
Кнопка «Далее» (next() → событие *:next) |
Прокидывать inQuestionBank в Alpine через Twig alpineConfig (как в QuestionChoice.twig / QuestionOpen.twig). В банке флаг выставляет PHP-transformer секции вопросов (inQuestionBank = true).
LMS: store questions
Заголовок раздела «LMS: store questions»На submit() вопроса вызывать writeQuestion + buildQuestionState.
Минимальный shape (Elementor-совместимый):
{ isQuestionCompleted: true, attemptsLeft: 0, attemptStatus: 0, nextButtonClicked: false, answers: [], // choice: 1-based индексы; иначе [] points: 0 | 1, showCorrectAnswersButton: false, // расширения (не ломают Elementor readers): // answer, answerType — open // order: string[] — sort // matchAnswers: { [promptId]: answerId } — match // placement: { [itemId]: categoryId|null } — classify}После клика «Далее» в банке вызывать markQuestionNextClicked (nextButtonClicked: true). Если у вопроса showFeedback: false, банк сам вызывает markQuestionNextClicked перед auto-goNext.
Правила:
- Писать только при стабильном
wp2Id(isStableLmsIdвlms-quiz.js). Не использовать ephemeralq-{index}как ключ store. - На
init()—readQuestion(wp2Id)и hydrate UI, еслиisQuestionCompleted. - Банк сам обновляет
quizчерезrecordQuizAnswerпо событию*:answered— вопрос не должен вызыватьaddQuizScore, если он внутри банка (иначе двойной счёт). Standalone вне банка пишет толькоquestions.
Подробнее: LMS_QUIZ_STORAGE.md.
Чеклист: новый тип (пример «Сортировка»)
Заголовок раздела «Чеклист: новый тип (пример «Сортировка»)»Имена ниже — шаблон. Замените sort / Sort / QuestionSort на свой slug.
A. Базовый wp2turbo-блок (как любой интерактив)
Заголовок раздела «A. Базовый wp2turbo-блок (как любой интерактив)»См. AI_TURBO_BLOCKS.md. Кратко:
blocks/sort-question/block.json+src/index.js(+ child-блоки при необходимости).- Атрибут
bankQuestionNumber(number, default 0) — для List View / заголовка в банке. - Общий prompt: переиспользовать
wp2turbo/question-prompt+getBankQuestionHeadingприbankQuestionNumber > 0. components/QuestionSort/QuestionSort.twig+schema.jsonс"requiresAlpine": true(в turbo или Blocks override).assets/src/frontend/question-sort.js→Alpine.data('turboQuestionSort', …); регистрация вassets/src/entries/turbo.js.src/Mapping/Wp2TurboSortQuestionTransformer.php— props:wp2Id,inQuestionBank: false, labels, данные задания.src/Blocks/RegisterBlocks.php—BlockMapping+ inserter allowlist (рядом с choice/open).
B. Совместимость с банком (обязательный слой)
Заголовок раздела «B. Совместимость с банком (обязательный слой)»Без этих правок блок не появится в банке и не попадёт в прогресс теста.
| # | Куда | Что сделать |
|---|---|---|
| 1 | blocks/_shared/question-types/registry.js |
Добавить { blockName, eventPrefix, component } — allowlist банка и Alpine listeners |
| 2 | add-questions-modal.js + index.js factories |
Опция в modal + QUESTION_FACTORIES → create…Block (allowlist уже из registry) |
| 3 | Тот же index.js → createQuestionBlocks |
Ветка factory → createSortQuestionBlock({ bankQuestionNumber }) |
| 4 | Новый blocks/sort-question/src/sort-question-sync.js на createQuestionSync |
createSortQuestionBlock(attributes) |
| 5 | Wp2TurboQuestionBankQuestionsTransformer.php |
В QUESTION_BLOCKS: 'wp2turbo/sort-question' => 'QuestionSort' |
| 6 | — | Слушатели в question-bank.js уже из registry (QUESTION_EVENT_PREFIXES) |
| 7 | question-bank/src/index.js |
Visibility / count уже через QUESTION_BLOCKS из registry |
| 8 | Twig QuestionSort |
inQuestionBank + кнопки Повторить / Далее; x-data с inQuestionBank |
| 9 | Alpine turboQuestionSort |
submit → writeQuestion + $dispatch('sort-question:answered', …); next → $dispatch('sort-question:next', …) |
Нумерация «Вопрос №N» уже синхронится в question-bank-questions через bankQuestionNumber + managed heading prompt — factory должна передавать номер и использовать shared prompt.
C. Сборка и проверка
Заголовок раздела «C. Сборка и проверка»npm run build:skin # или build:all из корняnpm test --prefix wp2lms-turbo-pulseРучной чеклист:
- Inserter: блок виден вне банка.
- Банк → «Добавить вопрос» → тип «Сортировка» → несколько штук → заголовки №N, разные
wp2Id. - Пройти тест: ответ → «Далее» → следующий / results.
- DevTools:
localStorage.questions/.quiz(вне курса) после ответа и finish. - Reload: hydrate completed / статус банка.
- Retry: wipe +
quiz:reset.
Минимальный скелет Alpine (шаблон)
Заголовок раздела «Минимальный скелет Alpine (шаблон)»import { buildQuestionState, readQuestion, writeQuestion, isStableLmsId } from './lib/lms-quiz.js';
export function registerQuestionSort(Alpine) { Alpine.data('turboQuestionSort', (initial = {}) => ({ answered: false, isCorrect: false, wp2Id: String(initial.wp2Id ?? '').trim(), inQuestionBank: Boolean(initial.inQuestionBank), showFeedback: initial.showFeedback !== false, // … состояние задания …
init() { this.hydrateFromLms(); },
hydrateFromLms() { if (!isStableLmsId(this.wp2Id)) return; const stored = readQuestion(this.wp2Id); if (!stored?.isQuestionCompleted) return; // восстановить UI из stored this.isCorrect = Number(stored.points) > 0; this.answered = true; },
persistToLms() { if (!isStableLmsId(this.wp2Id)) return; writeQuestion(this.wp2Id, buildQuestionState({ isCorrect: this.isCorrect, points: this.isCorrect ? 1 : 0, selectedIndexes: [], // order: [...], })); },
submit() { if (this.answered) return; this.isCorrect = /* проверка */; this.answered = true; this.persistToLms(); this.$dispatch('sort-question:answered', { wp2Id: this.wp2Id, isCorrect: this.isCorrect, hasFeedback: this.showFeedback, }); },
next() { this.$dispatch('sort-question:next', { wp2Id: this.wp2Id, isCorrect: this.isCorrect, }); },
reset() { this.answered = false; this.isCorrect = false; }, }));}В question-bank.js слушатели уже из registry — после добавления eventPrefix отдельный addEventListener не нужен.
A11y (обязательный контракт для проверочных типов)
Заголовок раздела «A11y (обязательный контракт для проверочных типов)»Общий хелпер: assets/src/frontend/lib/a11y.js.
| Требование | Как |
|---|---|
| Live-region | На корне вопроса/банка: [data-a11y-live] role="status" aria-live="polite" |
| После submit | announceAndFocusAfterAnswer → status [data-a11y-status] или Retry/Next [data-a11y-action] |
| После reset | focusAfterReset → первый [data-a11y-control] / input / select / button |
| Банк next/start/results | Фокус на [data-a11y-question-heading] / results title; progress aria-live |
| Sort | Кнопки ↑/↓ + drag (Sortable). Объявление позиции в live-region |
| Classify | <select> на карточке + drag. Drop zones с accessible name |
| Match | Listbox: aria-expanded / aria-controls / role="option"; Esc + стрелки; restore focus |
| Имена | Prompt → aria-labelledby (Choice fieldset, Open input). Не использовать placeholder как единственное имя |
| Focus visible | :focus-visible на кастомных контролах в questions.css |
| Motion | prefers-reduced-motion для анимации процента results |
Новый тип обязан: live-region, status/action markers, keyboard-путь к полному ответу (не только pointer/drag), вызов announce/focus после submit/reset.
Чего не делать
Заголовок раздела «Чего не делать»- Не писать прогресс напрямую в
localStorage/courseController— только черезlms-quiz.js/ pro globals. - Не вызывать
addQuizScoreиз самого вопроса внутри банка. - Не полагаться на индекс в массиве как на id в LMS — только
wp2Id. - Не добавлять тип только в modal без transformer
QUESTION_BLOCKS— на фронте вопрос «пропадёт» из банка. - Не трогать Scene /
page.progressиз вопроса или банка. - Не выпускать Sort/Classify/Match без клавиатурной альтернативы drag/popup.
Сводка файлов для нового проверочного типа
Заголовок раздела «Сводка файлов для нового проверочного типа»| # | Файл | Standalone | + Банк |
|---|---|---|---|
| 0 | blocks/_shared/question-types/registry.js |
— | ✓ |
| 1 | blocks/{name}/block.json + src/index.js |
✓ | ✓ |
| 2 | blocks/{name}/src/{name}-sync.js (createQuestionSync) |
— | ✓ |
| 3 | src/Mapping/Wp2Turbo{Name}Transformer.php |
✓ | ✓ (inQuestionBank default false) |
| 4 | src/Blocks/RegisterBlocks.php |
✓ | ✓ |
| 5 | components/Question{Name}/…twig |
✓ | ✓ (Далее / Повторить) |
| 6 | assets/src/frontend/question-{name}.js + turbo.js |
✓ | ✓ (события + LMS write + a11y) |
| 7 | question-bank-questions modal + factory |
— | ✓ |
| 8 | Wp2TurboQuestionBankQuestionsTransformer map |
— | ✓ |
| 9 | A11y: live-region, focus, keyboard path | ✓ | ✓ |
| 10 | Unit/e2e при необходимости | желательно | желательно |
После правок: npm run build:skin (блоки + Alpine) из wp2lms-turbo-blocks или build:all из корня.