Возобновление прерванного тура

Интерактивные туры, реализованные с помощью Intro.js, часто прерываются пользователем: закрытие вкладки, перезагрузка страницы, переход по ссылке или намеренное завершение. В результате пользователь теряет прогресс и при повторном запуске вынужден проходить шаги заново. Это снижает удобство и может негативно повлиять на восприятие интерфейса.

Решение заключается в сохранении состояния тура и его восстановлении при повторном входе.


Основы механизма возобновления

Intro.js не предоставляет встроенного механизма автоматического восстановления тура, однако предоставляет необходимые события и методы для реализации этой логики:

  • onbeforeexit — вызывается перед выходом из тура
  • onexit — вызывается при завершении
  • onchange — вызывается при переходе между шагами
  • currentStep() — возвращает индекс текущего шага
  • goToStep(step) — позволяет перейти к конкретному шагу

Основная идея:

  1. Сохранять номер текущего шага
  2. При повторной загрузке страницы проверять наличие сохранённого состояния
  3. Продолжать тур с нужного шага

Сохранение текущего шага

Для хранения состояния удобно использовать localStorage, так как он сохраняет данные между перезагрузками.

const intro = introJs();

intro.onchange(function() {
    const step = this.currentStep();
    localStorage.setItem('introStep', step);
});

Каждый раз при переходе к новому шагу текущий индекс сохраняется.


Обработка выхода из тура

Важно различать два сценария:

  • пользователь намеренно завершил тур
  • тур был прерван (например, перезагрузка страницы)

Для этого можно использовать onexit:

intro.onexit(function() {
    localStorage.removeItem('introStep');
});

Такой подход очищает состояние при нормальном завершении тура.

Если требуется сохранять прогресс даже при выходе, удаление можно не выполнять.


Восстановление тура при загрузке страницы

При инициализации страницы необходимо проверить, существует ли сохранённый шаг:

const savedStep = localStorage.getItem('introStep');

if (savedStep !== null) {
    intro.start().goToStep(Number(savedStep) + 1);
} else {
    intro.start();
}

Особенности:

  • Intro.js использует 1-based индексацию в goToStep
  • currentStep() возвращает 0-based индекс

Поэтому добавляется +1.


Защита от некорректных данных

Возможны ситуации, когда:

  • структура тура изменилась
  • шаг больше не существует
  • данные повреждены

Проверка:

const savedStep = parseInt(localStorage.getItem('introStep'), 10);

if (!isNaN(savedStep) && savedStep >= 0) {
    intro.start().goToStep(savedStep + 1);
} else {
    intro.start();
}

Работа с динамическими элементами

Если элементы тура загружаются динамически (например, через AJAX), важно убедиться, что DOM полностью готов перед восстановлением:

window.addEventListener('load', function() {
    const savedStep = localStorage.getItem('introStep');

    if (savedStep !== null) {
        intro.start().goToStep(Number(savedStep) + 1);
    }
});

В сложных случаях может понадобиться ожидание конкретного элемента:

function waitForElement(selector, callback) {
    const interval = setInterval(() => {
        if (document.querySelector(selector)) {
            clearInterval(interval);
            callback();
        }
    }, 100);
}

Использование sessionStorage

Если требуется сохранять прогресс только в рамках одной сессии (до закрытия вкладки), вместо localStorage используется sessionStorage:

sessionStorage.setItem('introStep', step);

Разница:

  • localStorage — сохраняется навсегда
  • sessionStorage — очищается при закрытии вкладки

Хранение дополнительного состояния

В сложных сценариях одного номера шага недостаточно. Можно сохранять:

  • текущую страницу
  • параметры пользователя
  • контекст интерфейса

Пример:

const state = {
    step: intro.currentStep(),
    url: window.location.pathname
};

localStorage.setItem('introState', JSON.stringify(state));

Восстановление:

const state = JSON.parse(localStorage.getItem('introState'));

if (state && state.url === window.location.pathname) {
    intro.start().goToStep(state.step + 1);
}

Возобновление после перехода между страницами

Если тур охватывает несколько страниц, необходимо:

  1. Сохранять шаг и текущий URL
  2. При переходе проверять соответствие страницы
  3. Продолжать тур только на нужной странице

Иногда используется параметр в URL:

window.location.href = '/next-page?introStep=3';

И затем:

const params = new URLSearchParams(window.location.search);
const step = params.get('introStep');

if (step) {
    intro.start().goToStep(Number(step));
}

Управление состоянием через события

Intro.js позволяет гибко контролировать поведение через события:

intro.onbeforechange(function(targetElement) {
    // логика перед переходом
});

intro.oncomplete(function() {
    localStorage.removeItem('introStep');
});

Это позволяет:

  • очищать состояние только при полном завершении
  • логировать поведение пользователя
  • синхронизировать тур с бизнес-логикой

Сброс состояния тура

Иногда требуется принудительно сбросить прогресс:

function resetTour() {
    localStorage.removeItem('introStep');
}

Или при определённых условиях:

if (userRoleChanged) {
    resetTour();
}

Особенности при SPA (Single Page Application)

В одностраничных приложениях (React, Vue, Angular):

  • переходы происходят без перезагрузки
  • состояние можно хранить в глобальном store (Redux, Pinia и т.д.)

Пример:

store.set('introStep', intro.currentStep());

При повторной инициализации компонента:

const step = store.get('introStep');

if (step !== null) {
    intro.start().goToStep(step + 1);
}

Частые ошибки

1. Несоответствие индексов

  • забывают про разницу между 0-based и 1-based

2. Запуск до загрузки DOM

  • элемент ещё не существует → тур ломается

3. Отсутствие очистки состояния

  • пользователь застревает на старом шаге

4. Изменение структуры тура

  • сохранённый шаг больше не актуален

Рекомендации по архитектуре

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

Пример:

const TOUR_VERSION = 'v1';

localStorage.setItem('introVersion', TOUR_VERSION);

Проверка:

if (localStorage.getItem('introVersion') !== TOUR_VERSION) {
    localStorage.removeItem('introStep');
}

Расширенный пример

const intro = introJs();

function saveStep() {
    localStorage.setItem('introStep', intro.currentStep());
}

function loadStep() {
    return parseInt(localStorage.getItem('introStep'), 10);
}

intro.onchange(saveStep);

intro.oncomplete(() => {
    localStorage.removeItem('introStep');
});

intro.onexit(() => {
    // можно оставить прогресс
});

window.addEventListener('load', () => {
    const step = loadStep();

    if (!isNaN(step)) {
        intro.start().goToStep(step + 1);
    } else {
        intro.start();
    }
});

Такой подход обеспечивает:

  • устойчивость к перезагрузкам
  • гибкое управление поведением
  • расширяемость под сложные сценарии