parent
5e2854d969
commit
4ad630c6ae
57
README.md
57
README.md
@ -6,12 +6,13 @@ _Guide your users in the one true path._
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Abraham injects dynamically-generated [Shepherd](https://shepherdjs.dev/) JavaScript code into your Rails application whenever a user should see a guided tour. Skip a tour, and we'll try again next time; complete a tour, and it won't show up again.
|
Abraham makes it easy to show guided tours to users of your Rails application. When Abraham shows a tour, it keeps track of whether the user has completed it (so it doesn't get shown again) or dismissed it for later (so it reappears in a future user session).
|
||||||
|
|
||||||
* Define tour content with simple YAML files, in any/many languages.
|
* Define tour content with simple YAML files, in any/many languages.
|
||||||
* Organize tours by controller and action.
|
* Organize tours by controller and action.
|
||||||
* Plays nicely with Turbolinks.
|
* Trigger tours automatically on page load or manually via JavaScript method.
|
||||||
* Ships with two basic CSS themes (default & dark) -- or write your own
|
* Built with the [Shepherd JS](https://shepherdjs.dev/) library. Plays nicely with Turbolinks.
|
||||||
|
* Ships with two basic CSS themes (default & dark) — or write your own
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ $ rails db:migrate
|
|||||||
Install the JavaScript dependencies:
|
Install the JavaScript dependencies:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ yarn add jquery@^3.4.0 js-cookie@^2.2.0 shepherd.js@^6.0.0-beta
|
$ yarn add js-cookie@^2.2.0 shepherd.js@^6.0.0-beta
|
||||||
```
|
```
|
||||||
|
|
||||||
Require `abraham` in `app/assets/javascripts/application.js`
|
Require `abraham` in `app/assets/javascripts/application.js`
|
||||||
@ -77,7 +78,7 @@ Tell Abraham where to insert its generated JavaScript in `app/views/layouts/appl
|
|||||||
|
|
||||||
## Defining your tours
|
## Defining your tours
|
||||||
|
|
||||||
Define your tours in the `config/tours` directory. Its directory structure should mirror your application's controllers, and the tour files should mirror your actions/views.
|
Define your tours in the `config/tours` directory corresponding to the views defined in your application. Its directory structure mirrors your application's controllers, and the tour files mirror your actions/views.
|
||||||
|
|
||||||
```
|
```
|
||||||
config/
|
config/
|
||||||
@ -92,11 +93,15 @@ config/
|
|||||||
└── show.es.yml
|
└── show.es.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
NB: You must specify a locale in the filename, even if you're only supporting one language.
|
For example, per above, when a Spanish-speaking user visits `/articles/`, they'll see the tours defined by `config/tours/articles/index.es.yml`.
|
||||||
|
|
||||||
|
(Note: You must specify a locale in the filename, even if you're only supporting one language.)
|
||||||
|
|
||||||
### Tour content
|
### Tour content
|
||||||
|
|
||||||
A tour is composed of a series of steps. A step may have a title and must have a description. You may attach a step to a particular element on the page, and place the callout in a particular position (see below).
|
Within a tour file, each tour is composed of a series of **steps**. A step may have a `title` and must have `text`. You may attach a step to a particular element on the page, and place the callout in a particular position.
|
||||||
|
|
||||||
|
In this example, we define a tour called "intro" with 3 steps:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
intro:
|
intro:
|
||||||
@ -129,7 +134,7 @@ When you specify an `attachTo` element, use the `placement` option to choose whe
|
|||||||
* `bottom left`
|
* `bottom left`
|
||||||
* `bottom right`
|
* `bottom right`
|
||||||
* `center` / `middle` / `middle center`
|
* `center` / `middle` / `middle center`
|
||||||
* `left` / `middle left'
|
* `left` / `middle left`
|
||||||
* `right` / `middle right`
|
* `right` / `middle right`
|
||||||
* `top` / `top center`
|
* `top` / `top center`
|
||||||
* `top left`
|
* `top left`
|
||||||
@ -140,6 +145,38 @@ Abraham tries to be helpful when your tour steps attach to page elements that ar
|
|||||||
* If your first step is attached to a particular element, and that element is not present on the page, the tour won't start. ([#28](https://github.com/actmd/abraham/issues/28))
|
* If your first step is attached to a particular element, and that element is not present on the page, the tour won't start. ([#28](https://github.com/actmd/abraham/issues/28))
|
||||||
* If your tour has an intermediate step attached to a missing element, Abraham will skip that step and automatically show the next. ([#6](https://github.com/actmd/abraham/issues/6))
|
* If your tour has an intermediate step attached to a missing element, Abraham will skip that step and automatically show the next. ([#6](https://github.com/actmd/abraham/issues/6))
|
||||||
|
|
||||||
|
### Automatic vs. manual tours
|
||||||
|
|
||||||
|
By default, Abraham will automatically start a tour that the current user hasn't seen yet. You can instead define a tour to be triggered manually using the `trigger` option:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
walkthrough:
|
||||||
|
trigger: "manual"
|
||||||
|
steps:
|
||||||
|
1:
|
||||||
|
text: "This walkthrough will show you how to..."
|
||||||
|
```
|
||||||
|
|
||||||
|
This tour will not start automatically; instead, use the `Abraham.startTour` method with the tour name:
|
||||||
|
|
||||||
|
```
|
||||||
|
<button id="startTour">Start tour</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelector("#startTour").addEventListener("click", function() {
|
||||||
|
Abraham.startTour("walkthrough"));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
...or if you happen to use jQuery:
|
||||||
|
|
||||||
|
```
|
||||||
|
<script>
|
||||||
|
$("#startTour").on("click", function() { Abraham.startTour('walkthrough'); })
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
### Testing your tours
|
### Testing your tours
|
||||||
|
|
||||||
Abraham loads tour definitions once when you start your server. Restart your server to see tour changes.
|
Abraham loads tour definitions once when you start your server. Restart your server to see tour changes.
|
||||||
@ -154,11 +191,11 @@ end
|
|||||||
|
|
||||||
## Full example
|
## Full example
|
||||||
|
|
||||||
We provide a [small example app](https://github.com/actmd/abraham-example) that implements abraham, so you can see it in action.
|
We provide a [small example app](https://github.com/actmd/abraham-example) that implements Abraham, so you can see it in action.
|
||||||
|
|
||||||
## Upgrading from version 1
|
## Upgrading from version 1
|
||||||
|
|
||||||
Abraham v1 was built using Shepherd 1.8, v2 now uses Shepherd 6 -- quite a jump, yes.
|
Abraham v1 was built using Shepherd 1.8, v2 now uses Shepherd 6 – quite a jump, yes.
|
||||||
|
|
||||||
If you were using Abraham v1, you'll want to take the following steps to upgrade:
|
If you were using Abraham v1, you'll want to take the following steps to upgrade:
|
||||||
|
|
||||||
|
@ -1,7 +1,28 @@
|
|||||||
//= require js-cookie/src/js.cookie
|
//= require js-cookie/src/js.cookie
|
||||||
//= require shepherd.js/dist/js/shepherd
|
//= require shepherd.js/dist/js/shepherd
|
||||||
|
|
||||||
|
var Abraham = new Object();
|
||||||
|
|
||||||
|
Abraham.tours = {};
|
||||||
|
Abraham.incompleteTours = [];
|
||||||
|
Abraham.startTour = function(tourName) {
|
||||||
|
if (!Shepherd.activeTour) {
|
||||||
|
Abraham.tours[tourName].start();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Abraham.startNextIncompleteTour = function() {
|
||||||
|
if (Abraham.incompleteTours.length) {
|
||||||
|
Abraham.tours[Abraham.incompleteTours[0]].checkAndStart();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", Abraham.startNextIncompleteTour);
|
||||||
|
document.addEventListener("turbolinks:load", Abraham.startNextIncompleteTour);
|
||||||
|
|
||||||
document.addEventListener('turbolinks:before-cache', function() {
|
document.addEventListener('turbolinks:before-cache', function() {
|
||||||
// Remove visible product tours
|
// Remove visible product tours
|
||||||
document.querySelectorAll(".shepherd-element").forEach(function(el) { el.remove() });
|
document.querySelectorAll(".shepherd-element").forEach(function(el) { el.remove() });
|
||||||
|
// Clear Abraham data
|
||||||
|
Abraham.tours = {};
|
||||||
|
Abraham.incompleteTours = [];
|
||||||
});
|
});
|
||||||
|
@ -4,23 +4,31 @@ module AbrahamHelper
|
|||||||
def abraham_tour
|
def abraham_tour
|
||||||
# Do we have tours for this controller/action in the user's locale?
|
# Do we have tours for this controller/action in the user's locale?
|
||||||
tours = Rails.configuration.abraham.tours["#{controller_name}.#{action_name}.#{I18n.locale}"]
|
tours = Rails.configuration.abraham.tours["#{controller_name}.#{action_name}.#{I18n.locale}"]
|
||||||
|
# Otherwise, default to the default locale
|
||||||
tours ||= Rails.configuration.abraham.tours["#{controller_name}.#{action_name}.#{I18n.default_locale}"]
|
tours ||= Rails.configuration.abraham.tours["#{controller_name}.#{action_name}.#{I18n.default_locale}"]
|
||||||
|
|
||||||
if tours
|
if tours
|
||||||
|
# Have any automatic tours been completed already?
|
||||||
completed = AbrahamHistory.where(
|
completed = AbrahamHistory.where(
|
||||||
creator_id: current_user.id,
|
creator_id: current_user.id,
|
||||||
controller_name: controller_name,
|
controller_name: controller_name,
|
||||||
action_name: action_name
|
action_name: action_name
|
||||||
)
|
)
|
||||||
remaining = tours.keys - completed.map(&:tour_name)
|
|
||||||
|
|
||||||
if remaining.any?
|
tour_keys_completed = completed.map(&:tour_name)
|
||||||
# Generate the javascript snippet for the next remaining tour
|
tour_keys = tours.keys
|
||||||
render(partial: "application/abraham",
|
|
||||||
locals: { tour_name: remaining.first,
|
tour_html = ''
|
||||||
steps: tours[remaining.first]["steps"] })
|
|
||||||
|
tour_keys.each do |key|
|
||||||
|
tour_html += render(partial: "application/abraham",
|
||||||
|
locals: { tour_name: key,
|
||||||
|
tour_completed: tour_keys_completed.include?(key),
|
||||||
|
trigger: tours[key]["trigger"],
|
||||||
|
steps: tours[key]["steps"] })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
tour_html.html_safe
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
var tour = new Shepherd.Tour(<%= Rails.configuration.abraham.tour_options.html_safe unless Rails.configuration.abraham.tour_options.nil? %>);
|
Abraham.tours["<%= tour_name %>"] = new Shepherd.Tour(<%= Rails.configuration.abraham.tour_options.html_safe unless Rails.configuration.abraham.tour_options.nil? %>);
|
||||||
|
|
||||||
tour.on("complete", function() {
|
<% if trigger != 'manual' %>
|
||||||
|
Abraham.tours["<%= tour_name %>"].on("complete", function() {
|
||||||
// Make AJAX call to save history of tour completion
|
// Make AJAX call to save history of tour completion
|
||||||
return fetch("/abraham_histories/", {
|
return fetch("/abraham_histories/", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -15,12 +16,13 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
tour.on("cancel", function() {
|
Abraham.tours["<%= tour_name %>"].on("cancel", function() {
|
||||||
Cookies.set('<%= abraham_cookie_prefix %>-<%= tour_name %>', 'later', { domain: '<%= abraham_domain %>' });
|
Cookies.set('<%= abraham_cookie_prefix %>-<%= tour_name %>', 'later', { domain: '<%= abraham_domain %>' });
|
||||||
});
|
});
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% steps.each_with_index do |(key, step), index| %>
|
<% steps.each_with_index do |(key, step), index| %>
|
||||||
tour.addStep({
|
Abraham.tours["<%= tour_name %>"].addStep({
|
||||||
id: 'step-<%= key %>',
|
id: 'step-<%= key %>',
|
||||||
<% if step.key?('title') %>
|
<% if step.key?('title') %>
|
||||||
title: "<%= step['title'] %>",
|
title: "<%= step['title'] %>",
|
||||||
@ -34,35 +36,40 @@
|
|||||||
},
|
},
|
||||||
<% end %>
|
<% end %>
|
||||||
buttons: [
|
buttons: [
|
||||||
<% if index == 0 %>
|
<% if index == steps.size - 1 %>
|
||||||
{ text: '<%= t('abraham.later') %>', action: tour.cancel, classes: 'shepherd-button-secondary' },
|
{ text: '<%= t('abraham.done') %>', action: Abraham.tours["<%= tour_name %>"].complete }
|
||||||
{ text: '<%= t('abraham.continue') %>', action: tour.next }
|
|
||||||
<% else %>
|
<% else %>
|
||||||
<% if index == steps.size - 1 %>
|
<% if index == 0 %>
|
||||||
{ text: '<%= t('abraham.done') %>', action: tour.complete }
|
{ text: '<%= t('abraham.later') %>', action: Abraham.tours["<%= tour_name %>"].cancel, classes: 'shepherd-button-secondary' },
|
||||||
<% else %>
|
{ text: '<%= t('abraham.continue') %>', action: Abraham.tours["<%= tour_name %>"].next }
|
||||||
{ text: '<%= t('abraham.exit') %>', action: tour.cancel, classes: 'shepherd-button-secondary' },
|
<% else %>
|
||||||
{ text: '<%= t('abraham.next') %>', action: tour.next }
|
{ text: '<%= t('abraham.exit') %>', action: Abraham.tours["<%= tour_name %>"].cancel, classes: 'shepherd-button-secondary' },
|
||||||
<% end %>
|
{ text: '<%= t('abraham.next') %>', action: Abraham.tours["<%= tour_name %>"].next }
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
tour.start = function (start) {
|
<% if trigger != "manual" %>
|
||||||
return function () {
|
Abraham.tours["<%= tour_name %>"].checkAndStart = function (start) {
|
||||||
// Don't start the tour if the user dismissed it once this session
|
return function () {
|
||||||
var tourMayStart = !Cookies.get('<%= abraham_cookie_prefix %>-<%= tour_name %>', {domain: '<%= abraham_domain %>'});
|
// Don't start the tour if the user dismissed it once this session
|
||||||
<% if steps.first[1]['attachTo'] %>
|
var tourMayStart = !Cookies.get('<%= abraham_cookie_prefix %>-<%= tour_name %>', {domain: '<%= abraham_domain %>'});
|
||||||
// Don't start the tour if the first step's element is missing
|
<% if steps.first[1]['attachTo'] %>
|
||||||
tourMayStart = tourMayStart && document.querySelector("<%= steps.first[1]['attachTo']['element'] %>");
|
// Don't start the tour if the first step's element is missing
|
||||||
<% end %>
|
tourMayStart = tourMayStart && document.querySelector("<%= steps.first[1]['attachTo']['element'] %>");
|
||||||
|
<% end %>
|
||||||
if (tourMayStart) {
|
|
||||||
start();
|
if (tourMayStart) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}(Abraham.tours["<%= tour_name %>"].start)
|
||||||
}(tour.start)
|
|
||||||
|
<% if !tour_completed %>
|
||||||
|
Abraham.incompleteTours.push("<%= tour_name %>");
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
tour.start()
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,8 +1,26 @@
|
|||||||
<h1>Dashboard#home</h1>
|
<h1>Dashboard#home</h1>
|
||||||
<p>Find me in app/views/dashboard/home.html.erb</p>
|
<p>Find me in app/views/dashboard/home.html.erb</p>
|
||||||
|
|
||||||
|
<%= link_to "Other Page", dashboard_other_url %>
|
||||||
|
|
||||||
<div class="notice-me" style="width:300px;height:300px;background-color:whitesmoke;">
|
<div class="notice-me" style="width:300px;height:300px;background-color:whitesmoke;">
|
||||||
a content element to notice
|
a content element to notice
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= link_to "Other Page", dashboard_other_url %>
|
<button id="restart_automatic">Restart the automatic tour</button>
|
||||||
|
<button id="show_manual">Show manual tour</button>
|
||||||
|
<button id="show_another_manual">Show ANOTHER manual tour</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelector("#restart_automatic").addEventListener("click", function() {
|
||||||
|
Abraham.startTour("intro");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("#show_manual").addEventListener("click", function() {
|
||||||
|
Abraham.startTour("a_manual_tour");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("#show_another_manual").addEventListener("click", function() {
|
||||||
|
Abraham.startTour("another_manual_tour");
|
||||||
|
});
|
||||||
|
</script>
|
@ -1,2 +1,4 @@
|
|||||||
<h1>Dashboard#other</h1>
|
<h1>Dashboard#other</h1>
|
||||||
<p>Find me in app/views/dashboard/other.html.erb</p>
|
<p>Find me in app/views/dashboard/other.html.erb</p>
|
||||||
|
|
||||||
|
<%= link_to "Home Page", dashboard_home_url %>
|
@ -11,6 +11,10 @@
|
|||||||
<body>
|
<body>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p><em>current_user.id = <%= current_user.id %></em></p>
|
||||||
|
|
||||||
<%= abraham_tour %>
|
<%= abraham_tour %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -17,3 +17,13 @@ intro:
|
|||||||
attachTo:
|
attachTo:
|
||||||
element: ".notice-me"
|
element: ".notice-me"
|
||||||
placement: "right"
|
placement: "right"
|
||||||
|
a_manual_tour:
|
||||||
|
trigger: manual
|
||||||
|
steps:
|
||||||
|
1:
|
||||||
|
text: "You triggered the manual tour"
|
||||||
|
another_manual_tour:
|
||||||
|
trigger: manual
|
||||||
|
steps:
|
||||||
|
1:
|
||||||
|
text: "You triggered the OTHER manual tour"
|
@ -17,6 +17,11 @@ class ToursTest < ApplicationSystemTestCase
|
|||||||
assert_selector ".shepherd-button", text: "Continue"
|
assert_selector ".shepherd-button", text: "Continue"
|
||||||
find(".shepherd-button", text: "Continue").click
|
find(".shepherd-button", text: "Continue").click
|
||||||
|
|
||||||
|
# Now try to manually trigger another tour
|
||||||
|
find('#show_manual').click
|
||||||
|
# Even though we triggered another tour, it should not appear since one is already active
|
||||||
|
assert_selector ".shepherd-element", count: 1, visible: true
|
||||||
|
|
||||||
# Tour Step 2
|
# Tour Step 2
|
||||||
assert_selector ".shepherd-header", text: "ENGLISH This step has a title"
|
assert_selector ".shepherd-header", text: "ENGLISH This step has a title"
|
||||||
assert_selector ".shepherd-text", text: "ENGLISH This intermediate step has some text"
|
assert_selector ".shepherd-text", text: "ENGLISH This intermediate step has some text"
|
||||||
@ -40,6 +45,17 @@ class ToursTest < ApplicationSystemTestCase
|
|||||||
# Tour should not reappear on reload
|
# Tour should not reappear on reload
|
||||||
visit dashboard_home_url
|
visit dashboard_home_url
|
||||||
refute_selector ".shepherd-element"
|
refute_selector ".shepherd-element"
|
||||||
|
|
||||||
|
# Now start a manual tour
|
||||||
|
find('#show_manual').click
|
||||||
|
assert_selector ".shepherd-element", visible: true
|
||||||
|
assert_selector ".shepherd-text", text: "You triggered the manual tour"
|
||||||
|
assert_selector ".shepherd-button", text: "Done"
|
||||||
|
find(".shepherd-button", text: "Done").click
|
||||||
|
|
||||||
|
# Even though we finished the manual tour, we can start it again right away
|
||||||
|
find('#show_manual').click
|
||||||
|
assert_selector ".shepherd-element", visible: true
|
||||||
end
|
end
|
||||||
|
|
||||||
test "mark a tour for Later and it will not come back in this session" do
|
test "mark a tour for Later and it will not come back in this session" do
|
||||||
@ -77,4 +93,22 @@ class ToursTest < ApplicationSystemTestCase
|
|||||||
# No tour should be visible, since the first step is invalid
|
# No tour should be visible, since the first step is invalid
|
||||||
refute_selector ".shepherd-element"
|
refute_selector ".shepherd-element"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "page with two incomplete tours shows them on consecutive visits" do
|
||||||
|
# First tour should appear at first visit
|
||||||
|
visit dashboard_other_url
|
||||||
|
assert_selector ".shepherd-element", visible: true
|
||||||
|
assert_selector ".shepherd-header", text: "TOUR ONE step one ENGLISH"
|
||||||
|
find(".shepherd-button", text: "Done").click
|
||||||
|
|
||||||
|
# Second tour should appear at second visit
|
||||||
|
visit dashboard_other_url
|
||||||
|
assert_selector ".shepherd-element", visible: true
|
||||||
|
assert_selector ".shepherd-header", text: "TOUR TWO step one ENGLISH"
|
||||||
|
find(".shepherd-button", text: "Done").click
|
||||||
|
|
||||||
|
# Now no tours should appear since they're both done
|
||||||
|
visit dashboard_other_url
|
||||||
|
refute_selector ".shepherd-element"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user