/
pavlovado
/
testingSubsystem
Обзор
Документация
Войти
/
pavlovado
/
testingSubsystem
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
static/js/element.js
965 строк
43 KB
pavlovado
source code
26 май 2025, 13:55
26 май 2025, 13:55
c0a20f4
Код
Авторство
О чём код?
// Объявление кастомного элемента SurveyComponent class SurveyComponent extends HTMLElement { constructor() { super(); // Получение шаблона по id survey-template const template = document.getElementById('survey-template'); if (!template) { console.error('Template #survey-template not found'); return; } this.attachShadow({ mode: 'open' }).appendChild(template.content.cloneNode(true)); this.isAnswerSelected = false; this.isSubmitted = false; } connectedCallback() { // Устанавливаем UUID опроса this.uuid = "1f54b84c-f7ce-49b1-9f6e-bd8c9800b2e5"; if (!this.uuid) { console.error("UUID не найден"); return; } // Задаем ID респондента по умолчанию this.dataset.respondentId = "default-respondent_test"; this.loadSurvey(); // Предупреждение при закрытии страницы с незавершенным опросом window.addEventListener('beforeunload', (event) => { if (Object.keys(this.answers).length > 0) { event.preventDefault(); event.returnValue = 'У вас есть несохраненные ответы. Вы уверены, что хотите уйти?'; } }); // Обработка перехода в оффлайн window.addEventListener('offline', () => { this.showError("Вы оффлайн. Ответы сохраняются локально.", true); }); // Обработка возврата в онлайн window.addEventListener('online', () => { this.removeErrorMessage("Вы оффлайн. Ответы сохраняются локально."); const savedState = localStorage.getItem(`survey_${this.uuid}`); if (savedState) { this.showError("Соединение восстановлено!", false, "success-message"); if (this.isSubmitted) { this.showError("Соединение восстановлено!", false, "success-message"); this.submitSurvey(); } } }); } // Метод загрузки данных опроса по UUID loadSurvey() { fetch(`/api/2.0/survey/${this.uuid}`) .then(async response => { if (!response.ok) throw new Error(await this.getUserFriendlyError(response)); return response.json(); }) .then(data => { this.surveyData = { ...data, title: data.survey_title || data.title || "Без названия", description: data.survey_description || data.description || "Описание отсутствует", }; // Проверка, есть ли локальное сохранение состояния const savedState = localStorage.getItem(`survey_${this.uuid}`); if (savedState) { const state = JSON.parse(savedState); console.log("Восстановленное состояние:", state); this.currentQuestionIndex = state.currentQuestionIndex || 0; this.answers = state.answers || {}; this.dataset.respondentId = state.respondent_id || this.dataset.respondentId; this.startSurveyFromState(); } else { // Если состояния нет — начать с интро this.currentQuestionIndex = -1; this.answers = {}; this.renderIntro(); } }) // Обработка ошибок загрузки опроса .catch(error => { console.error("Ошибка загрузки опроса:", error); this.shadowRoot.querySelector("slot[name='questions']").innerHTML = `<div style="background: #ffe5e5; padding: 1em; border-radius: 8px; color: #a00;"> <strong>Ошибка загрузки опроса</strong><br>${error.message} </div>`; this.shadowRoot.querySelector("#survey-title").classList.add("hidden"); this.shadowRoot.querySelector("#startButton")?.classList.add("hidden"); this.shadowRoot.querySelector("#intro").classList.add("hidden"); }); } // Метод отображения вводной части опроса renderIntro() { const root = this.shadowRoot; // Получение элементов интро const surveyTitle = root.querySelector("#survey-title"); const surveyDescription = root.querySelector("#survey-description"); const startButton = root.querySelector("#startButton"); const introContainer = root.querySelector("#intro"); if (!surveyTitle || !surveyDescription || !startButton || !introContainer) { console.error('One or more intro elements not found:', { surveyTitle, surveyDescription, startButton, introContainer }); return; } // Установка текста и элементов surveyTitle.textContent = this.surveyData.title; surveyDescription.textContent = this.surveyData.description; introContainer.classList.remove("hidden"); surveyDescription.classList.remove("hidden"); startButton.classList.remove("hidden"); // Обработчик нажатия кнопки "Начать" startButton.addEventListener("click", () => this.startSurvey()); } // Метод начала опроса (отображение вопросов) startSurvey() { this.currentQuestionIndex = 0; const root = this.shadowRoot; // Скрытие вступления и показ вопросов и навигации root.querySelector("#intro").classList.add("hidden"); root.querySelector("#questions").classList.remove("hidden"); root.querySelector("#navigation").classList.remove("hidden"); root.querySelector("#pagination").classList.remove("hidden"); // Отображение первого вопроса this.renderQuestion(); // Сохранение состояния this.saveStateToLocalStorage(); } // Метод отображения текущего вопроса renderQuestion() { const slot = this.shadowRoot.querySelector("slot[name='questions']"); slot.innerHTML = ""; const question = this.surveyData.questions[this.currentQuestionIndex]; const questionDiv = document.createElement("div"); questionDiv.className = "question-container"; // Вставка заголовка и описания вопроса questionDiv.innerHTML = ` <p><strong>${question.question_title}</strong></p> ${question.question_description ? `<p style="font-size: 0.9em; color: #666;">${question.question_description}</p>` : ""} `; this.isAnswerSelected = false; // Получение ранее сохраненных ответов const savedAnswers = this.answers[question.question_uuid] || []; // Обработка checkbox-вопросов if (question.question_type === "checkbox") { // Установка ограничения по количеству обязательных ответов const minCount = question.question_count_of_answers || 1; question.answers.forEach(answer => { // Создание базовой разметки const label = document.createElement("label"); const input = document.createElement("input"); input.type = "checkbox"; input.name = question.question_uuid; input.value = answer.answer_uuid; input.setAttribute("aria-describedby", `checkbox-help-${question.question_uuid}`); // Проверка, является ли вариант пользовательским const isCustom = answer.answer_title.toLowerCase().includes("свой вариант"); // Создание поля ввода для "своего варианта" const customInputContainer = document.createElement("div"); customInputContainer.className = "custom-input-container"; customInputContainer.style.display = isCustom && savedAnswers.includes(answer.answer_uuid) ? "block" : "none"; const customInput = document.createElement("input"); customInput.type = "text"; customInput.className = "custom-input"; customInput.placeholder = "Введите свой ответ"; const counter = document.createElement("span"); counter.className = "char-counter"; const minSymbols = answer.answer_min_symbols || 32; counter.textContent = `0/${minSymbols}`; customInputContainer.append(customInput, counter); // Восстановление состояния из сохраненного ответа if (savedAnswers.includes(answer.answer_uuid)) { input.checked = true; if (isCustom) { const customValue = savedAnswers.find(val => !question.answers.some(a => a.answer_uuid === val)); customInput.value = customValue || ""; counter.textContent = `${customInput.value.length}/${minSymbols}`; this.isAnswerSelected = customInput.value.trim().length >= minSymbols; } else { this.isAnswerSelected = savedAnswers.length >= minCount; } } input.addEventListener("change", () => { // Показать/скрыть поле "Свой вариант" customInputContainer.style.display = input.checked ? "block" : "none"; const selected = Array.from(questionDiv.querySelectorAll("input[type='checkbox']:checked")).map(el => el.value); const freeAnswer = question.answers.find(a => a.answer_type === "free"); const minSymbols = freeAnswer?.answer_min_symbols || 32; // Проверка, валиден ли пользовательский ввод const isValidCustom = !isCustom || !input.checked || (input.checked && customInput.value.trim().length >= minSymbols); // Валидация и сохранение состояния this.isAnswerSelected = isValidCustom && selected.length >= minCount; this.answers[question.question_uuid] = selected; // Обновление интерфейса и состояния this.updateNavigation(); this.updatePagination(); this.saveStateToLocalStorage(); }); customInput.addEventListener("input", () => { // Обновление счетчика символов counter.textContent = `${customInput.value.length}/${minSymbols}`; if (input.checked) { const trimmed = customInput.value.trim(); // Получение списка UUID всех выбранных чекбоксов const selected = Array.from(questionDiv.querySelectorAll("input[type='checkbox']:checked")).map(el => el.value); if (trimmed) selected.push(trimmed); this.answers[question.question_uuid] = selected; this.isAnswerSelected = selected.length >= minCount && trimmed.length >= minSymbols; // Обновление интерфейса и состояния this.updateNavigation(); this.updatePagination(); this.saveStateToLocalStorage(); } }); label.append(input, document.createTextNode(answer.answer_title)); questionDiv.append(label, document.createElement("br")); if (isCustom) { questionDiv.append(customInputContainer, document.createElement("br")); } }); // Обработка radio-вопросов } else if (question.question_type === "radio") { question.answers.forEach(answer => { const label = document.createElement("label"); const input = document.createElement("input"); input.type = "radio"; input.name = question.question_uuid; input.value = answer.answer_uuid; // Проверка, является ли ответ "своим вариантом" const isCustom = answer.answer_title.toLowerCase().includes("свой вариант"); const customInputContainer = document.createElement("div"); customInputContainer.className = "custom-input-container"; // Проверка, сохранен ли ранее выбранный ответ const isSaved = savedAnswers.length > 0 && (savedAnswers[0] === answer.answer_uuid || (isCustom && !question.answers.some(a => a.answer_uuid === savedAnswers[0]))); customInputContainer.style.display = isCustom && isSaved ? "block" : "none"; // Создание текстового поля для ввода "Своего варианта" const customInput = document.createElement("input"); customInput.type = "text"; customInput.className = "custom-input"; customInput.placeholder = "Введите свой ответ"; // Счетчик символов const counter = document.createElement("span"); counter.className = "char-counter"; const minSymbols = answer.answer_min_symbols || 32; counter.textContent = isSaved && isCustom ? `${(savedAnswers[0] || "").length}/${minSymbols}` : `0/${minSymbols}`; customInputContainer.append(customInput, counter); // Если ранее был сохранен этот ответ — отметить радиокнопку if (isSaved) { input.checked = true; if (isCustom) { customInput.value = savedAnswers[0] || ""; this.isAnswerSelected = customInput.value.trim().length >= minSymbols; } else { this.isAnswerSelected = true; } } // Обработчик выбора радиокнопки input.addEventListener("change", () => { questionDiv.querySelectorAll(".custom-input-container").forEach(container => container.style.display = "none"); if (input.checked) { if (isCustom) { customInputContainer.style.display = "block"; this.isAnswerSelected = customInput.value.trim().length >= minSymbols; if (!this.isAnswerSelected) { this.answers[question.question_uuid] = []; } } else { this.isAnswerSelected = true; this.answers[question.question_uuid] = [input.value]; } } else { this.isAnswerSelected = false; this.answers[question.question_uuid] = []; } this.updateNavigation(); this.updatePagination(); this.saveStateToLocalStorage(); }); // Обработчик ввода текста в "свой вариант" customInput.addEventListener("input", () => { counter.textContent = `${customInput.value.length}/${minSymbols}`; if (input.checked) { const trimmedValue = customInput.value.trim(); this.isAnswerSelected = trimmedValue.length >= minSymbols; this.answers[question.question_uuid] = trimmedValue ? [trimmedValue] : []; this.updateNavigation(); this.updatePagination(); this.saveStateToLocalStorage(); } }); label.append(input, document.createTextNode(answer.answer_title)); questionDiv.append(label); if (isCustom) { questionDiv.append(customInputContainer); } questionDiv.appendChild(document.createElement("br")); }); // Обработка text-вопросов } else if (question.question_type === "text") { const customInputContainer = document.createElement("div"); customInputContainer.className = "custom-input-container"; // Создание textarea const input = document.createElement("textarea"); input.className = "custom-input"; input.placeholder = "Введите свой ответ"; // Если есть сохраненный ответ, посдтавить его в поле input.value = savedAnswers[0] || ""; const counter = document.createElement("span"); counter.className = "char-counter"; // Минимальное количество символов для ответа, если оно задано в вопросе const minSymbols = question.answers?.[0]?.answer_min_symbols || 32; counter.textContent = `${input.value.length}/${minSymbols}`; customInputContainer.append(input, counter); // Обработчик изменения текста в textarea input.addEventListener("input", () => { const trimmedValue = input.value.trim(); counter.textContent = `${input.value.length}/${minSymbols}`; // Сохранение ответа this.answers[question.question_uuid] = [trimmedValue]; this.isAnswerSelected = trimmedValue.length >= minSymbols; this.updateNavigation(); this.updatePagination(); this.saveStateToLocalStorage(); }); questionDiv.appendChild(customInputContainer); if (minSymbols > 0) { } } slot.appendChild(questionDiv); this.updatePagination(); this.updateNavigation(); } // Метод для проверки валидности ответа isQuestionValid(question, answers) { // Проверка для вопроса с типом "checkbox" if (question.question_type === "checkbox") { const minCount = question.question_count_of_answers || 1; // Проверка, что количество выбранных ответов больше минимального количества const valid = answers.length >= minCount; const freeAnswer = question.answers.find(a => a.answer_type === "free"); // Валидным считается: // - достаточное количество выбранных вариантов, и // - если есть пользовательский ответ — его длина не меньше минимально допустимой if (freeAnswer) { const customValue = answers.find(val => !question.answers.some(a => a.answer_uuid === val)); return valid && (!customValue || customValue.trim().length >= (freeAnswer.answer_min_symbols || 32)); } return valid; } // Проверка для вопроса с типом "radio" if (question.question_type === "radio") { const valid = answers.length > 0; const freeAnswer = question.answers.find(a => a.answer_type === "free"); // Аналогично проверяем, что если выбран свободный ответ, // он должен иметь минимальную длину if (freeAnswer) { const customValue = answers.find(val => !question.answers.some(a => a.answer_uuid === val)); return valid && (!customValue || customValue.trim().length >= (freeAnswer.answer_min_symbols || 32)); } return valid; } // Проверка для вопроса с типом "text" if (question.question_type === "text") { // Проверка, что ответ есть, и его длина не меньше минимальной return (answers[0]?.trim() || "").length >= (question.answers?.[0]?.answer_min_symbols || 32); } // Для неизвестных типов вопросов считаем ответ невалидным return false; } // Метод для настройки пагинации updatePagination() { const pagination = this.shadowRoot.querySelector("#pagination"); pagination.innerHTML = ""; // Определение максимального индекса вопроса, к которому пользователь может перейти // Изначально считать, что доступен только первый вопрос let maxAccessibleIndex = 0; for (let i = 0; i < this.surveyData.questions.length; i++) { const question = this.surveyData.questions[i]; const savedAnswers = this.answers[question.question_uuid] || []; const isValid = this.isQuestionValid(question, savedAnswers); // Если ответ валиден, разрешаем переход к следующему вопросу // Устанавливаем максимальный индекс доступного вопроса на i+1 if (isValid) { maxAccessibleIndex = i + 1; } else { break; } } // Создание кнопки пагинации для каждого вопроса this.surveyData.questions.forEach((question, index) => { const button = document.createElement("button"); // Текст кнопки — номер вопроса button.textContent = index + 1; button.classList.add("pagination-button"); button.setAttribute("aria-label", `Перейти к вопросу ${index + 1}`); // Подсветить кнопку, если это текущий вопрос button.setAttribute("role", "button"); if (index === this.currentQuestionIndex) { button.classList.add("active"); button.setAttribute("aria-current", "true"); } else { button.setAttribute("aria-current", "false"); } // Проверка, был ли данный вопрос корректно отвечен const savedAnswers = this.answers[question.question_uuid] || []; const isAnswered = this.isQuestionValid(question, savedAnswers); // Разрешение на навигацию к вопросу, если его индекс не больше maxAccessibleIndex const canNavigate = index <= maxAccessibleIndex; // Добавление специального класса для отвеченного вопроса if (isAnswered) button.classList.add("answered"); // Если навигация к этому вопросу запрещена — блокируем кнопку if (!canNavigate) { button.disabled = true; button.classList.add("disabled"); button.setAttribute("aria-disabled", "true"); } else { button.setAttribute("aria-disabled", "false"); } // Обработчик клика по кнопке — переключаемся на выбранный вопрос, // только если навигация разрешена button.addEventListener("click", () => { if (canNavigate) { this.currentQuestionIndex = index; this.renderQuestion(); } }); pagination.appendChild(button); }); } // Метод для настройки навигации updateNavigation() { const root = this.shadowRoot; const prevButton = root.querySelector("#prevButton"); const nextButton = root.querySelector("#nextButton"); const submitButton = root.querySelector("#submitButton"); const question = this.surveyData.questions[this.currentQuestionIndex]; this.isAnswerSelected = this.isQuestionValid(question, this.answers[question.question_uuid] || []); // Показ кнопки "Назад", если это не первый вопрос, иначе скрыть prevButton.classList.toggle("hidden", this.currentQuestionIndex === 0); // Показ кнопки "Вперед", если не последний вопрос, иначе скрыть nextButton.classList.toggle("hidden", this.currentQuestionIndex >= this.surveyData.questions.length - 1); // Показ кнопки "Отправить", если текущий вопрос последний, иначе скрыть submitButton.classList.toggle("hidden", this.currentQuestionIndex < this.surveyData.questions.length - 1); // Блокировка кнопки "Вперед", если ответ на текущий вопрос не выбран или невалиден nextButton.disabled = !this.isAnswerSelected; nextButton.classList.toggle("disabled", !this.isAnswerSelected); nextButton.style.backgroundColor = nextButton.disabled ? "#ccc" : ""; // Аналогично для кнопки "Отправить" submitButton.disabled = !this.isAnswerSelected; submitButton.classList.toggle("disabled", !this.isAnswerSelected); submitButton.style.backgroundColor = submitButton.disabled ? "#ccc" : ""; // Обработчик клика для кнопки "Назад" prevButton.onclick = () => { this.currentQuestionIndex--; this.renderQuestion(); this.saveStateToLocalStorage(); }; // Обработчик клика для кнопки "Вперед" nextButton.onclick = () => { if (this.isAnswerSelected) { this.currentQuestionIndex++; this.renderQuestion(); this.saveStateToLocalStorage(); } }; // Обработчик клика для кнопки "Отправить" submitButton.onclick = () => { if (this.isAnswerSelected) this.showConfirmation(); }; } // Метод для отображения окна подтверждения showConfirmation() { const root = this.shadowRoot; root.querySelector("slot[name='questions']").innerHTML = ""; root.querySelector("#navigation-container").style.display = "none"; root.querySelector("#confirmation-container").style.display = "block"; // Базовое сообщение с вопросом о подтверждении отправки const confirmationMessage = root.querySelector("#confirmation-container p"); let message = "<strong>Вы уверены, что хотите отправить ответы?</strong>"; confirmationMessage.innerHTML = message; // Назначение обработчика на кнопку подтверждения отправки — вызов отправки опроса root.querySelector("#confirmSubmit").onclick = () => this.submitSurvey(); // Назначение обработчика на кнопку отмены — возврат к вопросам root.querySelector("#cancelSubmit").onclick = () => this.cancelSubmit(); } // Метод отмены отправки ответов cancelSubmit() { const root = this.shadowRoot; root.querySelector("#confirmation-container").style.display = "none"; root.querySelector("#navigation-container").style.display = "flex"; // Повторная отрисовка текущего вопроса, чтобы восстановить состояние интерфейса this.renderQuestion(); // Сохранение текущего состояния this.saveStateToLocalStorage(); // Если пользователь находится оффлайн — отображение предупреждения if (!navigator.onLine) { this.showError("Вы оффлайн. Ответы сохраняются локально.", true); } } // Метод подтверждения отправки submitSurvey() { this.isSubmitted = true; this.removeErrorMessage("Вы оффлайн. Ответы сохраняются локально."); // Формирование объекта payload с данными для отправки на сервер const payload = { // Уникальный идентификатор опроса survey_uuid: this.surveyData.survey_uuid || this.uuid, // ID респондента respondent_id: this.dataset.respondentId || null, // Список вопросов с их ответами questions: this.surveyData.questions.map(q => { const savedAnswer = this.answers[q.question_uuid] || []; const answers = []; // Обработка чекбоксов if (q.question_type === "checkbox") { savedAnswer.forEach(val => { const matching = q.answers.find(a => a.answer_uuid === val); if (matching) { answers.push({ answer_uuid: val }); } else { const customAnswer = q.answers.find(a => a.answer_type === "free"); if (customAnswer) { answers.push({ answer_uuid: customAnswer.answer_uuid, answer_text: val }); } } }); // Обработка radio } else if (q.question_type === "radio") { const selected = savedAnswer[0]; const matching = q.answers.find(a => a.answer_uuid === selected); if (matching && matching.answer_type === "free") { answers.push({ answer_uuid: matching.answer_uuid, answer_text: selected }); } else if (matching) { answers.push({ answer_uuid: selected }); } else { const customAnswer = q.answers.find(a => a.answer_type === "free"); if (customAnswer) { answers.push({ answer_uuid: customAnswer.answer_uuid, answer_text: selected }); } } // Обработка текстового вопроса } else if (q.question_type === "text" && savedAnswer[0]) { const textAnswer = q.answers?.[0]; if (textAnswer && textAnswer.answer_type === "free") { answers.push({ answer_uuid: textAnswer.answer_uuid, answer_text: savedAnswer[0] }); } else if (textAnswer) { answers.push({ answer_uuid: textAnswer.answer_uuid }); } } // Сбор данных по конкретному вопросу const questionPayload = { question_uuid: q.question_uuid, question_type: q.question_type, answers, }; // Дополнительное сохранение правильных ответов, если они есть if (q.correct_answers != null) { questionPayload.correct_answers = q.correct_answers; } // Аналогично ключевые слова if (Array.isArray(q.keywords) && q.keywords.length > 0) { questionPayload.keywords = q.keywords; } return questionPayload; }), }; // Функция для блокировки кнопок const disableButtons = () => { const confirmSubmit = this.shadowRoot.querySelector("#confirmSubmit"); const cancelSubmit = this.shadowRoot.querySelector("#cancelSubmit"); const retryButton = this.shadowRoot.querySelector(".retry-button"); if (confirmSubmit) { confirmSubmit.disabled = true; confirmSubmit.classList.add("disabled"); confirmSubmit.style.backgroundColor = "#ccc"; } if (cancelSubmit) { cancelSubmit.disabled = true; cancelSubmit.classList.add("disabled"); cancelSubmit.style.backgroundColor = "#ccc"; } if (retryButton) { retryButton.disabled = true; retryButton.classList.add("disabled"); retryButton.style.backgroundColor = "#ccc"; } }; // Функция для разблокировки кнопок const enableButtons = () => { const confirmSubmit = this.shadowRoot.querySelector("#confirmSubmit"); const cancelSubmit = this.shadowRoot.querySelector("#cancelSubmit"); const retryButton = this.shadowRoot.querySelector(".retry-button"); if (confirmSubmit) { confirmSubmit.disabled = false; confirmSubmit.classList.remove("disabled"); confirmSubmit.style.backgroundColor = ""; } if (cancelSubmit) { cancelSubmit.disabled = false; cancelSubmit.classList.remove("disabled"); cancelSubmit.style.backgroundColor = ""; } if (retryButton) { retryButton.disabled = false; retryButton.classList.remove("disabled"); retryButton.style.backgroundColor = ""; } }; // Отправка данных опроса на сервер const trySubmit = (retries = 3, delay = 5000) => { // Блокируем кнопки перед началом отправки disableButtons(); // POST-запрос на сервер fetch(`/api/2.0/addConductedSurvey/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }) .then(async response => { // Если ответ не OK (например, 500, 400, 403), обработка как ошибка if (!response.ok) { const responseBody = await response.text(); // Получение читаемого сообщения об ошибке const errorMessage = await this.getUserFriendlyError({ ...response, text: () => Promise.resolve(responseBody) }); this.showError(errorMessage); throw new Error(errorMessage); } const responseBody = await response.text(); const data = JSON.parse(responseBody); return data; }) .then(data => { // Успешно отправлено alert("Спасибо за участие в опросе!"); const retryButton = this.shadowRoot.querySelector(".retry-button"); if (retryButton) { retryButton.remove(); } // Удаление локально сохраненного состояния localStorage.removeItem(`survey_${this.uuid}`); // Удаление компонента с DOM this.remove(); }) // Обработка ошибки при отправке данных на сервер .catch(error => { console.error("Ошибка отправки:", error); // Если ошибка связана с сетью или пользователь оффлайн if (error.message.includes("Failed to fetch") || !navigator.onLine) { // Если остались попытки — попробовать снова if (retries > 0) { this.showError("Нет соединения с сервером. Повторная попытка через несколько секунд..."); setTimeout(() => trySubmit(retries - 1, delay), delay); // Попытки исчерпаны — сохранение ответов локально и повторная отправка вручную } else { this.showError("Не удалось отправить опрос. Ответы сохранены локально. Попробуйте снова позже."); const confirmationButtons = this.shadowRoot.querySelector("#confirmation-buttons"); if (!confirmationButtons) { console.error("Element #confirmation-buttons not found"); enableButtons(); return; } let retryButton = confirmationButtons.querySelector(".retry-button"); if (!retryButton) { // Создаём кнопку "Попробовать снова" retryButton = document.createElement("button"); retryButton.textContent = "Попробовать снова"; retryButton.classList.add("retry-button"); // Обработчик клика по кнопке "Попробовать снова" retryButton.onclick = () => { const errorBox = this.shadowRoot.querySelector("#survey-error"); if (errorBox) { const nonPersistentMessages = errorBox.querySelectorAll("div:not(.persistent-message)"); nonPersistentMessages.forEach(message => message.remove()); } // Повторный запуск отправки с новыми попытками trySubmit(3, 5000); }; // Скрыть кнопку "Отправить" const submitButton = this.shadowRoot.querySelector("#confirmSubmit"); if (submitButton) { submitButton.style.display = "none"; } confirmationButtons.appendChild(retryButton); } enableButtons(); } // Если ошибка не связана с сетью — показать сообщение } else { this.showError("Произошла ошибка при отправке опроса. Попробуйте снова позже."); const confirmationButtons = this.shadowRoot.querySelector("#confirmation-buttons"); if (!confirmationButtons) { console.error("Element #confirmation-buttons not found"); enableButtons(); return; } let retryButton = confirmationButtons.querySelector(".retry-button"); if (!retryButton) { retryButton = document.createElement("button"); retryButton.textContent = "Попробовать снова"; retryButton.classList.add("retry-button"); retryButton.onclick = () => { const errorBox = this.shadowRoot.querySelector("#survey-error"); if (errorBox) { const nonPersistentMessages = errorBox.querySelectorAll("div:not(.persistent-message)"); nonPersistentMessages.forEach(message => message.remove()); } trySubmit(3, 5000); }; // Скрыть кнопку "Отправить" const submitButton = this.shadowRoot.querySelector("#confirmSubmit"); if (submitButton) { submitButton.style.display = "none"; } confirmationButtons.appendChild(retryButton); enableButtons(); } } }); }; trySubmit(3, 5000); } // Метод для отображения сообщения об ошибке в интерфейсе showError(message, persistent = false, className = "") { const errorBox = this.shadowRoot.querySelector("#survey-error"); if (errorBox) { errorBox.innerHTML = ""; const messageDiv = document.createElement("div"); messageDiv.textContent = message; // Если сообщение должно быть постоянным, добавление специального класс и атрибута if (persistent) { messageDiv.classList.add("persistent-message"); messageDiv.setAttribute("data-message", message); } // Если указан дополнительный CSS-класс, применение его к сообщению if (className) { messageDiv.classList.add(className); } errorBox.appendChild(messageDiv); messageDiv.style.display = "block"; // Если сообщение не постоянное — автоматическое удаление через 5 секунд if (!persistent) { setTimeout(() => { messageDiv.remove(); }, 5000); } } else { console.error("Element #survey-error not found in shadow DOM"); } } // Метод для удаления из интерфейса сообщения об ошибке removeErrorMessage(message) { const errorBox = this.shadowRoot.querySelector("#survey-error"); if (errorBox) { const messageDiv = errorBox.querySelector(`[data-message="${message}"]`); if (messageDiv) { messageDiv.remove(); } } } // Метод отображений HTTP-ответа в понятное пользователю сообщение об ошибке async getUserFriendlyError(response, responseBody = null) { let message = `Ошибка ${response.status}`; try { const body = responseBody || await response.json(); if (response.status === 422) return "Упс! Эта ссылка на опрос не работает. Возможно, она устарела или содержит ошибку."; if (response.status === 404) return "Опрос не найден. Возможно, он был удален."; if (response.status === 400) return "Упс! Запрос не может быть обработан из-за ошибки в данных. Попробуйте снова."; if (response.status === 500) return "Внутренняя ошибка сервера. Попробуйте позже."; if (body?.detail) message += `: ${typeof body.detail === 'string' ? body.detail : JSON.stringify(body.detail)}`; } catch { message += `: ${responseBody || await response.text()}`; } return message; } // Метод дял сохранения данных в localStorage saveStateToLocalStorage() { // Сохранение uuid опроса, индекса текущего вопроса, ответов пользователя, // id респондента и метку времени для возможного восстановления прогресса const state = { survey_uuid: this.surveyData.survey_uuid || this.uuid, currentQuestionIndex: this.currentQuestionIndex, answers: this.answers, respondent_id: this.dataset.respondentId || null, timestamp: Date.now(), }; localStorage.setItem(`survey_${this.uuid}`, JSON.stringify(state)); } // Метод возобновления опроса, учитывая сохраненное состояние пользователя startSurveyFromState() { const root = this.shadowRoot; // Если текущий индекс вопроса >= 0, скрыть вступительный экран и показать вопросы, навигацию и пагинацию if (this.currentQuestionIndex >= 0) { root.querySelector("#intro").classList.add("hidden"); root.querySelector("#questions").classList.remove("hidden"); root.querySelector("#navigation").classList.remove("hidden"); root.querySelector("#pagination").classList.remove("hidden"); this.renderQuestion(); // Если индекс меньше 0 — показать вступительный экран } else { this.renderIntro(); } } } // Загрузка DOM-дерева страницы document.addEventListener("DOMContentLoaded", () => { const testContainer = document.getElementById("test-container"); testContainer.style.display = "block"; // Создание нового кастомного элемента опроса const survey = document.createElement("survey-component"); // Присвоение уникального идентификатор, UUID опроса и id респондента survey.setAttribute("id", "survey"); survey.setAttribute("uuid", "1f54b84c-f7ce-49b1-9f6e-bd8c9800b2e5"); survey.dataset.respondentId = "default-respondent_test"; testContainer.appendChild(survey); }); // Регистрация кастомного элемента с тегом <survey-component> и связывание его с классом SurveyComponent customElements.define("survey-component", SurveyComponent);