Вложенные туры

Вложенные туры представляют собой композицию нескольких независимых экземпляров тура, которые логически объединяются в единый сценарий. Такая структура применяется, когда требуется разбить сложный процесс обучения на несколько этапов, каждый из которых может запускаться, завершаться и управляться отдельно.

В библиотеке Shepherd.js отсутствует встроенное понятие «вложенного тура» как отдельной сущности. Вместо этого используется комбинация:

  • нескольких экземпляров Shepherd.Tour
  • событийной модели (on, once)
  • программного управления переходами между турами

Это даёт гибкость, но требует явного управления состоянием.


Базовая модель: несколько туров

Создание вложенной структуры начинается с объявления нескольких туров:

const mainTour = new Shepherd.Tour({
  defaultStepOptions: {
    cancelIcon: { enabled: true },
    scrollTo: true
  }
});

const subTour = new Shepherd.Tour({
  defaultStepOptions: {
    classes: 'shepherd-theme-dark',
    scrollTo: true
  }
});

Каждый тур полностью независим:

  • собственный набор шагов
  • собственные настройки
  • собственные события

Связывание туров

Связь между турами осуществляется вручную через события или кнопки шагов.

Запуск вложенного тура из шага

mainTour.addStep({
  id: 'step-with-subtour',
  text: 'Переход к дополнительному обучению',
  buttons: [
    {
      text: 'Далее',
      action: () => {
        mainTour.hide();
        subTour.start();
      }
    }
  ]
});

Ключевой момент — использование hide() вместо complete():

  • hide() временно скрывает текущий тур
  • позволяет вернуться к нему позже

Возврат в родительский тур

После завершения вложенного тура требуется восстановить основной:

subTour.on('complete', () => {
  mainTour.show('next-step-id');
});

или при отмене:

subTour.on('cancel', () => {
  mainTour.show('fallback-step-id');
});

Метод show(stepId) позволяет продолжить с конкретного шага.


Управление состоянием

При работе с вложенными турами возникает необходимость отслеживать:

  • текущий активный тур
  • точку возврата
  • пользовательские действия

Простейший менеджер состояния

let currentTour = null;

function startTour(tour) {
  if (currentTour) {
    currentTour.hide();
  }
  currentTour = tour;
  tour.start();
}

Каскад вложенных туров

Допускается создание глубокой вложенности:

  • основной тур

    • вложенный тур

      • вложенный под-тур

Пример цепочки

mainTour.addStep({
  id: 'step-1',
  buttons: [
    {
      text: 'Подтур',
      action: () => {
        mainTour.hide();
        subTour.start();
      }
    }
  ]
});

subTour.addStep({
  id: 'sub-step-1',
  buttons: [
    {
      text: 'Ещё глубже',
      action: () => {
        subTour.hide();
        subSubTour.start();
      }
    }
  ]
});

Возврат требует явного управления на каждом уровне.


Использование событий Shepherd

События — основной инструмент синхронизации.

Ключевые события:

  • start
  • show
  • hide
  • complete
  • cancel

Пример цепочки через события

mainTour.on('complete', () => {
  subTour.start();
});

subTour.on('complete', () => {
  finalTour.start();
});

Такой подход формирует линейный сценарий без явной вложенности в коде шагов.


Динамическое создание вложенных туров

Иногда вложенные туры создаются на лету в зависимости от контекста.

function createSubTour(userRole) {
  const tour = new Shepherd.Tour();

  if (userRole === 'admin') {
    tour.addStep({ text: 'Админские функции' });
  } else {
    tour.addStep({ text: 'Базовые функции' });
  }

  return tour;
}

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

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

Передача контекста между турами

Для сложных сценариев требуется передавать данные:

let context = {};

mainTour.addStep({
  id: 'collect-data',
  buttons: [
    {
      text: 'Сохранить',
      action: () => {
        context.userChoice = 'example';
        mainTour.next();
      }
    }
  ]
});

Во вложенном туре:

subTour.on('start', () => {
  console.log(context.userChoice);
});

Параллельные вложенные туры

Shepherd не поддерживает одновременное отображение нескольких туров. Однако можно имитировать переключение:

  • один тур скрывается
  • другой показывается

Важно избегать:

  • одновременного start() нескольких туров
  • конфликтов overlay

Общие ошибки

Потеря состояния

Использование complete() вместо hide():

mainTour.complete(); // невозможно вернуться

Правильный вариант:

mainTour.hide();

Отсутствие возврата

Если не подписаться на complete вложенного тура:

subTour.start();
// основной тур не возобновится

Конфликты DOM-элементов

Если шаги разных туров ссылаются на одинаковые элементы:

  • возможны скачки прокрутки
  • некорректное позиционирование

Рекомендуется:

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

Паттерн «Мастер–подзадачи»

Распространённый подход:

  • основной тур — навигация по разделам
  • вложенные туры — детальное обучение внутри разделов
Главный тур
 ├── Раздел A → Подтур A
 ├── Раздел B → Подтур B
 └── Раздел C → Подтур C

Реализация:

function attachSubTour(mainTour, subTour, triggerStepId, returnStepId) {
  mainTour.addStep({
    id: triggerStepId,
    buttons: [
      {
        text: 'Открыть',
        action: () => {
          mainTour.hide();
          subTour.start();
        }
      }
    ]
  });

  subTour.on('complete', () => {
    mainTour.show(returnStepId);
  });
}

Асинхронные вложенные туры

Иногда запуск подтура зависит от загрузки данных:

mainTour.addStep({
  id: 'async-step',
  buttons: [
    {
      text: 'Загрузить',
      action: async () => {
        mainTour.hide();
        await fetchData();
        subTour.start();
      }
    }
  ]
});

Важно учитывать:

  • задержки интерфейса
  • необходимость индикаторов загрузки

Интеграция с роутингом

В SPA-приложениях вложенные туры часто связаны с переходами между страницами:

mainTour.addStep({
  id: 'go-to-page',
  buttons: [
    {
      text: 'Перейти',
      action: () => {
        mainTour.hide();
        router.push('/settings');
      }
    }
  ]
});

После загрузки страницы:

router.afterEach(() => {
  subTour.start();
});

Управление стилями между турами

Каждый тур может иметь собственные темы:

const lightTour = new Shepherd.Tour({
  defaultStepOptions: {
    classes: 'shepherd-theme-light'
  }
});

const darkTour = new Shepherd.Tour({
  defaultStepOptions: {
    classes: 'shepherd-theme-dark'
  }
});

Это позволяет визуально отделять этапы обучения.


Тестирование вложенных туров

При тестировании необходимо проверять:

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

Частая практика — логирование:

mainTour.on('show', e => console.log('Main step:', e.step.id));
subTour.on('show', e => console.log('Sub step:', e.step.id));

Расширение через обёртки

Для упрощения работы создаются собственные абстракции:

class TourManager {
  constructor() {
    this.stack = [];
  }

  start(tour) {
    if (this.stack.length) {
      this.stack[this.stack.length - 1].hide();
    }
    this.stack.push(tour);
    tour.start();
  }

  end() {
    const finished = this.stack.pop();
    finished.complete();

    if (this.stack.length) {
      this.stack[this.stack.length - 1].show();
    }
  }
}

Такой подход:

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

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

Вложенные туры должны:

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

Рекомендуется:

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

Производительность

Большое количество туров может влиять на:

  • DOM-нагрузку
  • обработчики событий

Оптимизация:

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

Итоговая модель

Вложенные туры в Shepherd.js — это:

  • композиция независимых туров
  • управление через события
  • ручная синхронизация состояния

Гибкость реализации позволяет строить сложные обучающие сценарии, но требует строгой дисциплины в архитектуре и управлении потоками выполнения.