Extension:GuidedTour/Write an extension tour
This page explains how to write an extension tour. This is a ResourceLoader module you bundle into your extension to explain a UI you create or modify. It has full internationalization support, both for the common tour elements and the text you write. You use titlemsg, descriptionmsg, and namemsg to pass in i18n keys for your text.
Examples
editThe tours currently included in the GuidedTour extension can be viewed or downloaded via Gitiles.
A series of steps
editFirst, you have to consider when and how your tour should start.
The tour itself is a sequence of dialog windows that appear on the page, called steps. (It is possible to have a single-step tour that operates as a fancy tooltip, and to jump into the tour at different points.)
Each step can optionally have a next button, a back button, or both. You can also have the tour automatically move (transition) from one step to another in response to a user action (for instance, when the user makes their first change to an open VisualEditor window). You define the tour in a JavaScript function, calling API methods to construct and link the steps together.
The extension keeps track of the current step in each tour in a browser cookie. By default, a tour will continue across multiple web pages.
Implementation
edit- Consult the design recommendations and the API documentation.
- Choose a tour name. This should be set when constructing the
TourBuilder
(see example tours below).name: 'tourname',
- For each step in your tour:
- Choose title and description text.
- Then, choose whether the step will have an attachment or a central overlay:
- Attachment positions a step near a page element such as the Edit tab, a link, or the save button. You identify the page element using a selector. Any jQuery selector can be used. In general, this includes all CSS selectors. This example positions near the Edit tab using an id selector:
attachTo: '#ca-edit',
- For attachments, you must choose an attachment position. The choices are topLeft, top, topRight, rightTop, right, rightBottom, bottomRight, bottom, bottomLeft, leftBottom, left, leftTop. You can experiment with these values, and may want to test in a skin beside your primary one (using the useskin parameter). For on-wiki tours, you should test in the wiki's site direction (which does not vary by user or according to uselang). If you're not sure what this is, you can check in a JavaScript console with:
$( document.body ).is( '.sitedir-ltr' ) ? 'ltr' : 'rtl'
- Extension-defined tours should always be written left-to-right. In either case (on-wiki or extension tours), if the user is using another direction (for example, they are browsing English Wikipedia but have their interface language set to Hebrew in preferences), steps will automatically flip horizontally.
- By default (if you do not choose one of the directions above) the step shows in the center of the screen. This allows you to explain something without attaching to an element. In this case, it is recommended that you use an overlay to draw focus to the step:
overlay: true,
- Attachment positions a step near a page element such as the Edit tab, a link, or the save button. You identify the page element using a selector. Any jQuery selector can be used. In general, this includes all CSS selectors. This example positions near the Edit tab using an id selector:
- To link steps together, you can use Next and Back buttons. These will automatically be added if you tell the tour how to find the Next and Back steps. For example:
- For Next:
step.next( 'nextStep' );
- For Back:
step.back( 'previousStep' );
- Both Next and Back can be used on the same step. Also, more powerful behavior is available (such as choosing a different next or back step depending on context); see the 'next' and 'back' methods in the API documentation.
- For Next:
- Choose any further button actions you want to add to the step. If you want the user to click something (such as the edit tab), you can provide no explicit buttons (an Okay button that dismisses the step will still show). If you only want Next and/or Back buttons, you do not need to specify anything here (see above). Otherwise, you will want to specify one or more. You can specify arbitrary button actions. However, there are helpers included for common button behaviors. For
okay
andend
, no custom button text is necessary. For the others, usename
ornamemsg
(see below sections):okay
- Shows as an Okay button. Will run the specified function when the button is clicked.{ action: 'okay', onclick: function () { /* ... */ } }
end
- Shows as an Okay button. Will end the tour when clicked.{ action: 'end' }
wikiLink
- Shows as a textual button. Will go to the specified wiki page when clicked.{ action: 'wikiLink', page: 'Name of page' }
externalLink
- Shows as a textual button. Will go the specified external web site when clicked.{ action: 'externalLink', url: 'http://example.com' }
- For any of the buttons, you can specify the type of button as
neutral
,progressive
,constructive
,destructive
, orquiet
. See the Living Style Guide for an explanation of the meanings. For example:{ action: 'wikiLink', page: 'Name of page', type: 'neutral' }
- You can add arbitrary CSS classes to the button using the classString parameter. For example:
{ action: 'wikiLink', page: 'Name of page', type: 'neutral', cssClass: 'class_1 class_2' }
- By default, the step will close when they click outside it. In some cases, such as on the edit page, you may want to disable this by including:
closeOnClickOutside: false,
- Decide if you want transition behavior. In some cases, you will want to automatically transition to another step when an event (including page load) occurs. For example, if you have a step asking the user to click Edit, you may want to transition to a step pointing to the Preview button (
"explainPreviewButton"
in this example) once they're on the edit page. For more information about transitions, see the API documentation on the 'transition' method.step.transition( function () { if ( gt.isEditing() ) { return 'explainPreviewButton'; } } )
Note that if a step doesn't have a button to take them somewhere, you'll generally want to implement a transition()
function for the current step that allows them to go somewhere. Otherwise the tour will "stall" on it — every time the user reloads the page or visits any page that loads your tour, the current step will reappear until the user exits the tour.
Tips
edit- Use debug mode while developing your tours.
- Inspect the tour cookie (named
wgCookiePrefix-mw-tour
) to see what step your tour is on. - Even when writing an on-wiki tour, use a JavaScript checker such as jshint to check for errors in your tour definition.
- When stepping through your code in a browser debugger, the
show()
function inguiders.js
is one of the most useful functions in which to set a breakpoint.
General notes
editIf you will be using GuidedTour, you should list EventLogging and GuidedTour as dependencies in your README file and your Extension: page on this site.
Once it is a dependency, you must ensure GuidedTour is deployed anywhere your extension is. Remember that GuidedTour has its own localizations, which you may want to check, before deploying GuidedTour to a new wiki.
In many cases, you will want to refer to an existing interface message. MediaWiki lets you easily see the message keys for messages by adding ?uselang=qqx to the URL. For example, try this page. Then, you can include such messages in your own messages using the int magic word.
Adding a tour
editTour names must be unique for all tours on a given wiki, so make sure yours is descriptive. If you are writing a tour for including in the GuidedTour extension, no prefix is necessary. If it is in another extension, prefix it with your extension name to avoid collisions. For example, if your extension is named foobar, use something like foobarfeature.
A new tour should be saved under your extensions modules or resources folder, in a tours subdirectory. For example, resources/tours/foobarfeature.js.
To register it, use code similar to how a JavaScript resource loader is normally registered, but use the naming scheme ext.guidedTour.tour.mytourname. In this example, it would be ext.guidedTour.tour.foobarfeature. If you do not use this module naming scheme, the tour will not load.
$wgResourceModules['ext.guidedTour.tour.foobarfeature'] = array(
'scripts' => 'tours/foobarfeature.js',
// Tours must depend on GuidedTour.
'dependencies' => 'ext.guidedTour',
'messages' => array(
// This is a list of messages for your tour.
// You can include messages from your regular extension,
// or MediaWiki core, and you will also need messages specifically
// for the title, description and buttons of your tour.
// Note the naming scheme for tour-specific messages is based on your tour name.
// You do not need to add messages that are built-in to GuidedTour, like the okay button.
'guidedtour-tour-foobarfeature-some-step-title',
'guidedtour-tour-foobarfeature-some-step-description',
),
// localBasePath and remoteExtPath should match what your extension normally uses.
'localBasePath' => __DIR__ . '/resources',
'remoteExtPath' => 'Foo/resources',
);
You should add the messages you used the normal way (e.g., in en.json and qqq.json). Be sure to add qqq explanations.
Test tour code walkthrough
editThis is an annotated walkthrough of the test tour built into Extension:GuidedTour. The test tour does not use any multipage functionality (see #Firstedit tour code walkthrough).
/*
* Guided Tour to test guided tour features.
*/
// Copy the next line into the start of your tour.
( function ( window, document, $, mw, gt ) {
// Declare a variable for use later
var pageName = 'Help:Guided tours/guider',
tour;
tour = new gt.TourBuilder( {
/*
* This is the name of the tour. It must be lowercase, without any hyphen (-) or
* period (.) characters.
*
* Note this matches the tour name used elsewhere, such as in the module name.
*/
name: 'mytest'
} );
// Information defining each tour step
// This tour shows a central overlay at the start of the tour.
// Guiders appear in the center if another position is not specified.
// To specify the first step of the tour, use .firstStep instead of .step
tour.firstStep( {
name: 'overlay',
// For extension tours, you use message keys to allow internationalization.
// Anywhere you see msg:, the value should be a MediaWiki message key.
// Such keys must also be listed in your ResourceLoaderModule (see wgResourceModules above)
// and in your extension i18n files.
titlemsg: 'guidedtour-tour-test-testing',
// The description appears in the body
descriptionmsg: 'guidedtour-tour-test-test-description',
// This specifies that there is an overlay behind the guider.
overlay: true
} )
// This specifies the next step of the tour, and will automatically generate a next button.
// 'callout' refers to the name used in the step immediately below. Although putting the steps
// in a meaningful order is recommended, any step can be specified as next/back.
.next( 'callout' );
tour.step( {
/*
* Callout of left menu
*/
name: 'callout',
titlemsg: 'guidedtour-tour-test-callouts',
descriptionmsg: 'guidedtour-tour-test-portal-description',
// This positions the guider next to a page element, in this
// case the portal link (which is "Community portal" on English
// Wikipedia, but varies by site).
// The string is a jQuery selector. "#n-portal" means the HTML
// element with this id attribute, and "a" means an a, or link,
// element inside that.
attachTo: '#n-portal a',
// This means the guider shows to the right of the Community Portal link
position: 'right',
} )
.next( 'description' )
// The 'back' property specifies that you can go back from this step, and where to go
// if the back button is clicked.
.back( 'overlay' );
tour.step( {
/*
* Test out mediawiki description pages
*/
name: 'description',
titlemsg: 'guidedtour-tour-test-description-page',
overlay: true,
// This means the wikitext for the description will be loaded from the
// page name in the description field.
// Note that description: is used below, rather than descriptionmsg, in this case:
onShow: gt.getPageAsDescription,
// Name of the page to parse
description: pageName,
buttons: [ {
// This makes a button which acts like a wikilink to 'Help:Guided tours/guider'
action: 'wikiLink',
page: pageName,
namemsg: 'guidedtour-tour-test-go-description-page',
// This specifies that the button takes you to the next step of a process,
// which affects its appearance.
type: 'progressive'
}, {
// Note that when you use an action button, you don't have
to deal with internationalization (except wikiLink and externalLink) or a manual onclick.
// This makes the okay button on this step end the tour.
action: 'end'
} ]
} )
.back( 'callout' );
// The following should be the last line of your tour.
} ( window, document, jQuery, mediaWiki, mediaWiki.guidedTour ) );
Firstedit tour code walkthrough
editInternationalization messages
editThese are a few illustrative example messages from en.json. You should also add qqq.json message descriptions. Follow the 'en' link for the rest of the messages, and the 'qqq' one for the message descriptions:
The first line below is an example of how to refer to the text of an existing interface element, in this case the edit button.
"guidedtour-tour-firstedit-edit-page-description": "{{GENDER:|Click}} the \"{{int:vector-view-edit}}\" button to make your changes.",
"guidedtour-tour-firstedit-preview-title": "{{GENDER:|Preview}} your changes (optional)",
JavaScript walkthrough
editThis is a walkthrough of a simplified version of the firstedit tour. It covers more advanced features not described above:
// Guided Tour to help users make their first edit.
// Designed to work on any Wikipedia article, and can work for other sites with minor message changes.
( function ( window, document, $, mw, gt ) {
var hasEditSection, tour;
// Check if there are section edit links (used later)
hasEditSection = $( '.mw-editsection' ).length > 0;
tour = new gt.TourBuilder( {
name: 'myfirstedit',
// Specify that we want logging for this tour
shouldLog: true
} );
tour.firstStep( {
name: 'intro',
titlemsg: 'guidedtour-tour-firstedit-edit-page-title',
descriptionmsg: 'guidedtour-tour-firstedit-edit-page-description',
attachTo: '#ca-edit',
position: 'bottom',
// This indicates that we don't want an automatic next button,
// even though we are specifying which step comes next.
allowAutomaticNext: false,
buttons: [ {
// Custom logic to specify a button and its behavior
// depending on whether there are sections on the page.
action: hasEditSection ? 'next' : 'okay',
onclick: function () {
if ( hasEditSection ) {
mw.libs.guiders.next();
} else {
mw.libs.guiders.hideAll();
}
}
} ]
} )
// At certain times, called transition points, the callback passed to .transition
// will be called. At those times, this tour checks if the user is editing. If so,
// the tour returns 'preview', indicating that the tour should transition to the
// 'preview' step automatically.
.transition( function () {
if ( gt.isEditing() ) {
return 'preview';
}
} )
.next( 'editSection' );
tour.step( {
name: 'editSection',
titlemsg: 'guidedtour-tour-firstedit-edit-section-title',
descriptionmsg: 'guidedtour-tour-firstedit-edit-section-description',
position: 'right',
attachTo: '.mw-editsection',
// Automatically scroll to this step
autoFocus: true,
// Custom width, in pixels
width: 300
} )
.transition( function () {
if ( gt.isEditing() ) {
return 'preview';
} else if ( !hasEditSection ) {
// Returning HIDE means that the tour should be hidden, but not ended.
return gt.TransitionAction.HIDE;
}
} )
.back( 'intro' );
tour.step( {
name: 'preview',
titlemsg: 'guidedtour-tour-firstedit-preview-title',
descriptionmsg: 'guidedtour-tour-firstedit-preview-description',
attachTo: '#wpPreview',
autoFocus: true,
position: 'top',
// This specifies that, unlike the default, the guider should not close when the user clicks outside of it.
closeOnClickOutside: false
} )
.transition( function () {
// isReviewing checks whether the user is either previewing or showing changes.
if ( gt.isReviewing() ) {
return 'save';
} else if ( !gt.isEditing() ) {
// When the user is on this step, but neither editing nor reviewing, this tour automatically ends.
return gt.TransitionAction.END;
}
} )
.next( 'save' );
tour.step( {
name: 'save',
titlemsg: 'guidedtour-tour-firstedit-save-title',
descriptionmsg: 'guidedtour-tour-firstedit-save-description',
attachTo: '#wpSave',
autoFocus: true,
position: 'top',
closeOnClickOutside: false
} )
.transition( function () {
if ( !gt.isReviewing() ) {
return gt.TransitionAction.END;
}
} )
.back( 'preview' );
} ( window, document, jQuery, mediaWiki, mediaWiki.guidedTour ) );