Множественные туры на странице

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

Ключевой принцип — полная изоляция туров. Каждый тур:

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

Создание нескольких туров

Каждый тур создаётся как отдельный объект:

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

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

const onboardingT our = new Shepherd.Tour({
  useModalOverlay: true
});

Важно:

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

Организация шагов по турам

Каждый тур содержит только релевантные шаги:

mainTour.addStep({
  id: 'dashboard',
  text: 'Это главная панель',
  attachTo: {
    element: '.dashboard',
    on: 'bottom'
  }
});

settingsTour.addStep({
  id: 'profile-settings',
  text: 'Настройки профиля',
  attachTo: {
    element: '.profile-settings',
    on: 'right'
  }
});

Разделение по смыслу:

  • основной интерфейс
  • настройки
  • onboarding новых пользователей
  • новые функции (feature tours)

Управление запуском туров

Туры запускаются независимо:

mainTour.start();
settingsTour.start();

Однако важно избегать одновременного запуска нескольких туров, так как:

  • может возникнуть конфликт overlay
  • шаги перекрывают друг друга
  • пользовательский опыт ухудшается

Централизованное управление турами

Часто создаётся управляющий слой:

const tours = {
  main: mainTour,
  settings: settingsTour,
  onboarding: onboardingTour
};

function startTour(name) {
  Object.values(tours).forEach(tour => tour.cancel());
  tours[name].start();
}

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

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

Условный запуск туров

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

if (user.isNew) {
  onboardingTour.start();
} else if (feature.isUpdated) {
  featureTour.start();
}

Типичные условия:

  • первый вход пользователя
  • появление новой функциональности
  • изменение интерфейса
  • уровень доступа (роль)

Связывание туров между собой

Иногда требуется последовательный запуск:

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

Или:

onboardingTour.on('cancel', () => {
  console.log('Пользователь прервал onboarding');
});

Варианты связей:

  • автоматическое продолжение
  • ветвление сценариев
  • логирование поведения

Динамический выбор тура

Выбор тура может происходить на основе текущего маршрута:

switch (window.location.pathname) {
  case '/dashboard':
    mainTour.start();
    break;
  case '/settings':
    settingsTour.start();
    break;
}

В SPA-приложениях:

router.afterEach((to) => {
  if (to.name === 'dashboard') {
    mainTour.start();
  }
});

Повторное использование шагов

Некоторые шаги могут быть общими для нескольких туров. Возможен вынос в фабрику:

function createHelpStep(selector, text) {
  return {
    text,
    attachTo: {
      element: selector,
      on: 'bottom'
    }
  };
}

mainTour.addStep(createHelpStep('.help', 'Раздел помощи'));
settingsTour.addStep(createHelpStep('.help', 'Здесь помощь по настройкам'));

Преимущества:

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

Хранение состояния прохождения

Часто требуется запоминать, какие туры уже были показаны:

if (!localStorage.getItem('mainTourSeen')) {
  mainTour.start();
  localStorage.setItem('mainTourSeen', 'true');
}

Расширенный вариант:

const tourState = JSON.parse(localStorage.getItem('tours') || '{}');

if (!tourState.settings) {
  settingsTour.start();
  tourState.settings = true;
  localStorage.setItem('tours', JSON.stringify(tourState));
}

Прерывание и переключение туров

При переходе между разделами важно корректно завершать тур:

window.addEventListener('beforeunload', () => {
  Object.values(tours).forEach(tour => tour.cancel());
});

Или при смене вкладки:

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    mainTour.cancel();
  }
});

Асинхронные туры

Некоторые туры зависят от загрузки данных:

fetch('/api/data')
  .then(() => {
    dataTour.start();
  });

Или ожидание DOM:

const observer = new MutationObserver(() => {
  if (document.querySelector('.dynamic-element')) {
    dynamicTour.start();
    observer.disconnect();
  }
});

observer.observe(document.body, { childList: true, subtree: true });

Избежание конфликтов

Основные проблемы при множественных турах:

1. Дублирование overlay

  • решается отключением предыдущего тура

2. Конфликт фокуса

  • использовать cancel() перед start()

3. Перекрытие элементов

  • разные точки привязки (attachTo)

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

  • хранение в localStorage или state-менеджере

Модульная структура проекта

Рекомендуемая организация:

/tours
  mainTour.js
  settingsTour.js
  onboardingTour.js
  index.js

Пример index.js:

import mainTour from './mainTour';
import settingsTour from './settingsTour';

export const tours = {
  main: mainTour,
  settings: settingsTour
};

Расширенные сценарии

1. Feature tours (точечные подсказки)

const featureTour = new Shepherd.Tour();

featureTour.addStep({
  text: 'Новая кнопка!',
  attachTo: {
    element: '.new-feature',
    on: 'top'
  }
});

2. Контекстные туры

if (user.role === 'admin') {
  adminTour.start();
}

3. Частичные туры

Тур может начинаться не с первого шага:

mainTour.show('step-3');

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

Полезные приёмы:

mainTour.on('show', (event) => {
  console.log('Показ шага:', event.step.id);
});
mainTour.on('complete', () => {
  console.log('Тур завершён');
});

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

При большом количестве туров:

  • не создавать все туры сразу
  • использовать ленивую инициализацию
let settingsTour;

function getSettingsTour() {
  if (!settingsTour) {
    settingsTour = new Shepherd.Tour();
  }
  return settingsTour;
}

Масштабирование

При росте приложения:

  • использовать менеджер туров
  • хранить конфигурации отдельно
  • применять dependency injection
  • разделять по доменам

Типичные ошибки

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

Практика интеграции

Множественные туры становятся частью UX-стратегии:

  • onboarding — один раз
  • feature tours — при обновлениях
  • help tours — по запросу пользователя
  • contextual tours — по ситуации

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