/* global ve */ /** * GuidedTour public API * * Set as mw.guidedTour and often aliased to gt locally * * @author Terry Chay <tchay@wikimedia.org> * @author Matt Flaschen <mflaschen@wikimedia.org> * @author Ori Livneh <olivneh@wikimedia.org> * @author Rob Moen <rmoen@wikimedia.org> * @author S Page <spage@wikimedia.org> * @author Sam Smith <git@samsmith.io> * @author Luke Welling <lwelling@wikimedia.org> * * @class mw.guidedTour * @singleton */ /* * Part of GuidedTour, the MediaWiki extension for guided tours. * * Uses Optimize.ly's Guiders library (with customizations developed at WordPress * and MediaWiki). */ ( function ( guiders ) { 'use strict'; var gt = mw.guidedTour, internal = gt.internal, cookieName, cookieParams, // Initialized to false at page load // Will be set true any time postEdit fires, including right after // legacy wgPostEdit variable is set to true. isPostEdit = false; /** * Returns the current user state, initalizing it if needed * * @private * * @return {Object} user state object. If there is none, or the format was * invalid, returns a skeleton state object from * mw.guidedTour.internal#getInitialUserStateObject */ function getCookieState() { var cookieValue, parsed; cookieValue = mw.cookie.get( cookieName ); parsed = internal.parseUserState( cookieValue ); if ( parsed !== null ) { return parsed; } else { return internal.getInitialUserStateObject(); } } /** * Returns the current combined user state (cookie state and server-launched state) * Basically, this acts like the cookie state, except the server can specify * tours that take precedence via a $wg. * * @private * * @return {Object} combined state object. If there is none, or the format was * invalid, returns a skeleton state object from * mw.guidedTour.internal#getInitialUserStateObject */ function getUserState() { var cookieState = getCookieState(), serverState = mw.config.get( 'wgGuidedTourLaunchState' ), state = cookieState; if ( serverState !== null ) { state = $.extend( true, state, serverState ); } return state; } /** * Removes a tour from the cookie * * @private * * @param {string} tourName name of tour to remove * * @return {void} */ function removeTourFromUserStateByName( tourName ) { var parsedCookie = getCookieState(); delete parsedCookie.tours[ tourName ]; mw.cookie.set( cookieName, JSON.stringify( parsedCookie ), cookieParams ); } /** * Launch tour from given user state * * @private * * @param {Object} state State that specifies the tour progress * * // @return {boolean} Whether a tour was launched */ function launchTourFromState( state ) { var tourName, tourNames, candidateTours = []; for ( tourName in state.tours ) { candidateTours.push( { name: tourName, step: state.tours[ tourName ].step } ); } tourNames = candidateTours.map( function ( el ) { return el.name; } ); internal.loadMultipleTours( tourNames ) .always( function () { var tourName, max, currentStart; // This value is before 1970, but is a simple way // to ensure the comparison below always works. max = { startTime: -1 }; // Not all the tours in the cookie necessarily // loaded successfully, but the defined tours did. // So we make sure it is defined and in the user // state. for ( tourName in internal.definedTours ) { if ( state.tours[ tourName ] !== undefined && gt.shouldShowTour( { tourName: tourName, userState: state, pageName: mw.config.get( 'wgPageName' ), articleId: mw.config.get( 'wgArticleId' ), condition: internal.definedTours[ tourName ].showConditionally } ) ) { currentStart = state.tours[ tourName ].startTime || 0; if ( currentStart > max.startTime ) { max = { name: tourName, step: state.tours[ tourName ].step, startTime: currentStart }; } } } if ( max.name !== undefined ) { // Launch the most recently started tour // that meets the conditions. gt.launchTour( max.name, gt.makeTourId( max ) ); } } ); } // TODO (mattflaschen, 2013-07-10): Known issue: This runs too early on a direct // visit to a veaction=edit page. This probably affects other JS-generated // interfaces too. /** * Initializes guiders and shows tour, starting at the specified step. * Does not check conditions, so that should already be done * * @private * * @param {string} tourName name of tour * @param {string} tourId id to start at * * @return {void} * @throws {mw.guidedTour.IllegalArgumentError} If the tour ID is not consistent * with the tour name, or does not refer to a valid step */ function showTour( tourName, tourId ) { var tour, tourInfo; tour = internal.definedTours[ tourName ]; tourInfo = gt.parseTourId( tourId ); if ( tourInfo.name !== tourName ) { throw new gt.IllegalArgumentError( 'The tour ID "' + tourId + '" is not part of the tour "' + tourName + '".' ); } tour.showStep( tourInfo.step ); } /** * Guiders has a window resize and document ready listener. * * However, we're adding some MW-specific code. Currently, this listens for a * custom event from the WikiEditor extension, which fires after the extension's * async loop finishes. If WikiEditor is not running this event just won't fire. * * @private * * @return {void} */ function setupRepositionListeners() { $( '#wpTextbox1' ).on( 'wikiEditor-toolbar-doneInitialSections', guiders.reposition ); mw.hook( 've.skinTabSetupComplete' ).add( guiders.reposition ); } /** * Listen for events that may mean a tour should transition. * Currently this listens for some custom events from VisualEditor. * * @private */ function setupStepTransitionListeners() { // TODO (mattflaschen, 2014-03-17): Temporary hack, until // there are tour-level transition listeners. // Will also change as mediawiki.libs.guiders module is refactored. /** * Checks for a transition after a minimal timeout * * @param {mw.guidedTour.TransitionEvent} transitionEvent event that triggered * the check */ function transition( transitionEvent ) { // I found this timeout necessary when testing, probably to give the // browser queue a chance to do pending DOM rendering. setTimeout( function () { var currentStepInfo, currentStep, nextStep, tour; if ( guiders._currentGuiderID === null ) { // Ignore transitions if there is no active tour. return; } currentStepInfo = gt.parseTourId( guiders._currentGuiderID ); if ( currentStepInfo === null ) { mw.log.warn( 'Invalid _currentGuiderID. Returning early' ); return; } tour = internal.definedTours[ currentStepInfo.name ]; currentStep = tour.getStep( currentStepInfo.step ); nextStep = currentStep.checkTransition( transitionEvent ); if ( nextStep !== currentStep && nextStep !== null ) { tour.showStep( nextStep ); } }, 0 ); } // The next two are handled differently since they also require // settings an internal boolean. // TODO (mattflaschen, 2014-04-01): Hack pending tour-level listeners. mw.hook( 'postEdit' ).add( function () { var transitionEvent = new gt.TransitionEvent(); transitionEvent.type = gt.TransitionEvent.MW_HOOK; transitionEvent.hookName = 'postEdit'; transitionEvent.hookArguments = []; isPostEdit = true; transition( transitionEvent ); } ); } /** * Internal initialization of guiders and guidedtour, called once after singleton * is built. * * @private * * @return {void} */ function initialize() { // GuidedTour uses cookies to keep the user's progress when they are in the // tour, unless it's single-page. cookieName = '-mw-tour'; cookieParams = { expires: null }; // null means to use a session cookie. // Show X button guiders._defaultSettings.xButton = true; guiders._defaultSettings.autoFocus = true; guiders._defaultSettings.closeOnEscape = true; guiders._defaultSettings.closeOnClickOutside = true; guiders._defaultSettings.flipToKeepOnScreen = true; $( function () { setupRepositionListeners(); setupStepTransitionListeners(); } ); } // Add external API (internal API is at gt.internal) // Most, but not all, of this is public (non-public ones use standard // @private marking). $.extend( gt, { /** * Parses tour ID into an object with name and step keys. * * @param {string} tourId ID of tour/step combination * * @return {Object|null} Tour info object, or null if invalid input * @return {string} return.name Tour name * @return {string} return.step Tour step, always a string, but * either textual (e.g. 'preview') or numeric (e.g. '5') */ parseTourId: function ( tourId ) { // Keep in sync with regex in GuidedTourHooks.php var TOUR_ID_REGEX = /^gt-([^.-]+)-([^.-]+)$/, tourMatch, tourName, tourStep; if ( typeof tourId !== 'string' ) { return null; } tourMatch = tourId.match( TOUR_ID_REGEX ); if ( !tourMatch ) { return null; } tourName = tourMatch[ 1 ]; tourStep = tourMatch[ 2 ]; if ( tourName.length === 0 ) { return null; } return { name: tourName, step: tourStep }; }, /** * Serializes tour information into a string * * @param {Object} tourInfo * @param {string} tourInfo.name Tour name * @param {number|string} tourInfo.step Tour step, which can be a string, * such as 'preview', or numeric, as either a string ('5') or a * number (5). * * @return {string|null} ID of tour, or null if invalid input */ makeTourId: function ( tourInfo ) { if ( !$.isPlainObject( tourInfo ) ) { return null; } return 'gt-' + tourInfo.name + '-' + tourInfo.step; }, /** * Launch a tour. Tours start automatically if the environment is present * (user string or cookie). * * However, this method allows one tour to launch another. It also allows * callers to launch a tour on demand. * * The tour will only be shown if allowed by the specification (see defineTour). * * It will first try loading a tour module, then fall back on an on-wiki tour. * This means the caller doesn't need to know how it's implemented (which could * change). * * launchTour is used to load the tour specified in the URL too. This case * does not require an extra request for an extension-defined tour since it * is already loaded. * * `mw.guidedTour.launcher.launchTour` should always be used over this method. * * @private * * @param {string} tourName Name of tour * @param {string|null} [tourId='gt-' + tourName + '-' + step] ID of tour * and step. Omitted or null means to start the tour from the beginning. * * @return {void} */ launchTour: function ( tourName, tourId ) { internal.loadTour( tourName ).done( function () { var tour = internal.definedTours[ tourName ]; if ( tour && gt.shouldShowTour( { tourName: tourName, userState: getUserState(), pageName: mw.config.get( 'wgPageName' ), articleId: mw.config.get( 'wgArticleId' ), condition: tour.showConditionally } ) ) { if ( tourId ) { showTour( tourName, tourId ); } else { tour.start(); } } } ); }, /** * Attempts to launch a tour from the query string (tour parameter) * * @return {boolean} Whether a tour was launched */ launchTourFromQueryString: function () { var step, tourId, tourName = mw.util.getParamValue( 'tour' ); if ( tourName !== null && tourName.length !== 0 ) { step = gt.getStepFromQuery(); if ( step !== null && step !== '' ) { tourId = gt.makeTourId( { name: tourName, step: step } ); } else { tourId = null; } gt.launchTour( tourName, tourId ); return true; } return false; }, /** * Attempts to launch a tour from combined user state (cookie + tours launched * directly by server) * * @return {boolean} Whether a tour was launched */ launchTourFromUserState: function () { var state = getUserState(); return launchTourFromState( state ); }, /** * Attempts to automatically launch a tour based on the environment * * If the query string has a tour parameter, the method attempts to use that. * * Otherwise, the method tries to use the GuidedTour cookie. It checks which tours * are applicable to the current page. If more than one is, this method * loads the most recently started tour. * * If both fail, it does nothing. * * @return {void} */ launchTourFromEnvironment: function () { // Tour is either in the query string or cookie (prefer query string) if ( this.launchTourFromQueryString() ) { return; } this.launchTourFromUserState(); }, /** * Sets the tour cookie, given a tour name and optionally, a step. * * You can use this when you want the tour to be displayed on a future page. * If there is currently no cookie, it will set the start time. This * will not be done if only the step is changing. * * This does not take into account isSinglePage. * * @param {string} name Tour name * @param {number|string} [step=1] Tour step * * @return {void} */ setTourCookie: function ( name, step ) { step = step || 1; gt.updateUserStateForTour( { tourInfo: { name: name, step: step }, wasShown: false } ); }, // TODO (mattflaschen, 2014-04-04): Cleanup and move into Tour /** * Ends the tour, removing user's state * * @param {string} [tourName] tour to end, defaulting to most recent one * that showed a guider * * @return {void} */ endTour: function ( tourName ) { var guider, tourId, tourInfo, tour; if ( tourName !== undefined ) { removeTourFromUserStateByName( tourName ); } else { tourId = guiders._currentGuiderID; tourInfo = gt.parseTourId( tourId ); tourName = tourInfo.name; guider = guiders._guiderById( tourId ); gt.removeTourFromUserStateByGuider( guider ); } tour = internal.definedTours[ tourName ]; if ( tour.currentStep !== null ) { tour.currentStep.unregisterMwHooks(); } guiders.hideAll(); }, /** * Hides the guider(s) * * @return {void} */ hideAll: function () { guiders.hideAll(); }, // Begin onShow bindings section // // These are used as the value of the onShow field of a step. // These are deprecated. To allow async API calls, they are now // implemented another way in mw.GuidedTour.Step, but this is a temporary // backwards compatibility shim. /** * Parses description as wikitext * * Add this to onShow. * * @deprecated * * @param {Object} guider Guider object to set description on * * @return {void} */ parseDescription: 'parseDescription is not a real function', // Do not use mw.log.deprecate for this, since there is some magic // in StepBuilder that accesses it to check equality. /** * Parses a wiki page and uses the HTML as the description. * * To use this, put the page name as the description, and use this as the * value of onShow. * * @deprecated * * @param {Object} guider Guider object to set description on * * @return {void} */ getPageAsDescription: 'getPageAsDescription is not a real function', // End onShow bindings section // // Begin transition helpers // // These are utility functions useful in determining whether a step should // transition (e.g. move to a new step or hide the guider), and if so what // to do. /** * Checks whether user is on a particular wiki page. * * @param {string} pageName Expected page name * * @return {boolean} true if the page name is a strict match, false otherwise */ isPage: function ( pageName ) { return mw.config.get( 'wgPageName' ) === pageName; }, /** * Checks whether the query and pageName match the provided ones. * * It will return true if and only if the actual query string has all of the * mappings from queryParts (the actual query string may be a superset of the * expected), and pageName (optional) is exactly equal to wgPageName. * * If pageName is falsy, the page name will not be considered in any way. * * @param {Object} queryParts Object mapping expected query * parameter names (string) to expected values (string) * @param {string} [pageName] Page name * * @return {boolean} true if and only if there is a match per above */ hasQuery: function ( queryParts, pageName ) { var qname; if ( pageName && mw.config.get( 'wgPageName' ) !== pageName ) { return false; } for ( qname in queryParts ) { if ( mw.util.getParamValue( qname ) !== queryParts[ qname ] ) { return false; } } return true; }, /** * Checks if the user is editing, with either wikitext or the * VisualEditor. Does not include previewing. * * @return {boolean} true if and only if they are actively editing */ isEditing: function () { return gt.isEditingWithWikitext() || gt.isEditingWithVisualEditor(); }, /** * Checks if the user is editing with wikitext. Does not include previewing. * * @return {boolean} true if and only if they are on the edit action */ isEditingWithWikitext: function () { return mw.config.get( 'wgAction' ) === 'edit'; }, /** * Checks if the user is editing with VisualEditor. This is only true if * the surface is actually open for edits. * * Use isVisualEditorOpen instead if you want to check if there is a * VisualEditor instance on the page. * * @see mw.guidedTour#isVisualEditorOpen * * @return {boolean} true if and only if they are actively editing with VisualEditor */ isEditingWithVisualEditor: function () { return $( '.ve-ce-documentNode[contenteditable="true"]' ).length > 0; }, /** * Checks whether VisualEditor is open * * @return {boolean} true if and only if there is a VisualEditor instance * on the page */ isVisualEditorOpen: function () { return typeof ve !== 'undefined' && ve.instances && ve.instances.length > 0; }, // TODO: Doesn't currently detect reviewing with VE /** * Checks whether the user is previewing or reviewing changes * (after clicking "Show changes") * * @return {boolean} true if and only if they are reviewing */ isReviewing: function () { return gt.isReviewingWithWikitext(); }, /** * Checks whether the user is previewing or reviewing wikitext changes * (the latter meaning the screen after clicking "Show changes") * * @return {boolean} true if and only if they are reviewing wikitext */ isReviewingWithWikitext: function () { return mw.config.get( 'wgAction' ) === 'submit'; }, /** * Checks whether the user just saved an edit. * * You can also handle the 'postEdit' mw.hook in a * mw.guidedTour.StepBuilder#transition handler. * * This method is not necessary if post-edit is the only * criterion for the transition. * * @return {boolean} true if they just saved an edit, false otherwise */ isPostEdit: function () { return isPostEdit; }, // End transition helpers /** * Gets step of tour from querystring * * @private * * @return {string} Step */ getStepFromQuery: function () { return mw.util.getParamValue( 'step' ); }, /** * Resumes a loaded tour, specifying a tour and (optionally) a step. * * If no step is provided, it will first try to get a step from the URL. * * If that fails, it will try to resume from the cookie. * * Finally, it will default to step 1. * * @param {string} tourName Tour name * @param {number|string} [step] Step, defaulting to the cookie or first step of tour. * * @return {void} */ resumeTour: function ( tourName, step ) { var userState; if ( step === undefined ) { step = gt.getStepFromQuery() || 0; } userState = getUserState(); if ( ( step === 0 ) && userState.tours[ tourName ] !== undefined ) { // start from user state position showTour( tourName, gt.makeTourId( { name: tourName, step: userState.tours[ tourName ].step } ) ); } if ( step === 0 ) { step = 1; } // start from step specified showTour( tourName, gt.makeTourId( { name: tourName, step: step } ) ); }, /** * Removes the tour cookie for a given guider. * * @private * * @param {Object} guider any guider from the tour * * @return {void} */ removeTourFromUserStateByGuider: function ( guider ) { var tourInfo = gt.parseTourId( guider.id ); if ( tourInfo !== null ) { removeTourFromUserStateByName( tourInfo.name ); } }, /** * Updates a single tour in the user cookie state. The tour must already be loaded. * * @private * * @param {Object} args keyword arguments * @param {Object} args.tourInfo tour info object with name and step * @param {boolean} args.wasShown true if the guider was actually just shown on the * current page, false otherwise. Certain fields can only be initialized on a * page where it was shown. * * @return {void} */ updateUserStateForTour: function ( args ) { var cookieState = getCookieState(), tourName, tourSpec, articleId, pageName, cookieValue; tourName = args.tourInfo.name; // It should be defined, except when wasShown is false. tourSpec = internal.definedTours[ tourName ] || {}; // Ensure there's a sub-object for this tour if ( cookieState.tours[ tourName ] === undefined ) { cookieState.tours[ tourName ] = {}; cookieState.tours[ tourName ].startTime = Date.now(); } if ( args.wasShown && tourSpec.showConditionally === 'stickToFirstPage' && cookieState.tours[ tourName ].firstArticleId === undefined && cookieState.tours[ tourName ].firstSpecialPageName === undefined ) { articleId = mw.config.get( 'wgArticleId' ); if ( articleId !== 0 ) { cookieState.tours[ tourName ].firstArticleId = articleId; } else { pageName = mw.config.get( 'wgPageName' ); cookieState.tours[ tourName ].firstSpecialPageName = pageName; } } cookieState.tours[ tourName ].step = String( args.tourInfo.step ); cookieValue = JSON.stringify( cookieState ); mw.cookie.set( cookieName, cookieValue, cookieParams ); }, // Below are exposed for unit testing only, and should be considered // private /** * Returns cookie configuration, for testing only. * * @private * * @return {Object} cookie configuration */ getCookieConfiguration: function () { return { name: cookieName, parameters: cookieParams }; }, /** * Determines whether to show a given tour, given the name, full cookie * value, and condition specified in the tour definition. * * Exposed only for testing. * * @private * * @param {Object} args arguments * @param {string} args.tourName name of tour * @param {Object} args.userState full value of tour cookie, not null * @param {string} args.pageName current full page name (wgPageName format) * @param {string} args.articleId current article ID * @param {string} [args.condition] showIf condition specified in tour definition, if any * See mw.guidedTour.TourBuilder#constructor for usage * * @return {boolean} true to show, false otherwise * @throws {mw.guidedTour.TourDefinitionError} On invalid conditions */ shouldShowTour: function ( args ) { var subState = args.userState.tours[ args.tourName ]; if ( args.condition !== undefined ) { // TODO (mattflaschen, 2013-07-09): Allow having multiple // conditions ANDed together in an array. switch ( args.condition ) { case 'stickToFirstPage': if ( subState === undefined ) { // Not yet shown return true; } if ( subState.firstArticleId !== undefined ) { return subState.firstArticleId === args.articleId; } else if ( subState.firstSpecialPageName !== undefined ) { return subState.firstSpecialPageName === args.pageName; } break; case 'wikitext': // Any screen that is *not* VisualEditor-specific // Reading, history, wikitext-specific screens, etc. return !gt.isVisualEditorOpen(); case 'VisualEditor': // Any screen that is *not* wikitext-specific // Reading, history, VisualEditor screen, etc. return !gt.isEditingWithWikitext() && !gt.isReviewingWithWikitext(); default: throw new gt.TourDefinitionError( '\'' + args.condition + '\' is not a supported condition' ); } } // No conditions or inconsistent cookie data return true; } } ); /** * Creates a tour based on an object specifying it, but does not show * it immediately * * mw.guidedTour.Tour#constructor has details on tourSpec.name, * tourSpec.isSinglePage, and tourSpec.showConditionally. * * @method defineTour * @deprecated * * @param {Object} tourSpec object specifying tour * @param {Array} tourSpec.steps Array of steps; see * mw.guidedTour.TourBuilder#step. In addition, the following deprecated * option is supported only through defineTour. * @param {Function} [tourSpec.steps.shouldSkip] Function returning a * boolean, which specifies whether to skip the current step based on the * page state * @param {boolean} tourSpec.steps.shouldSkip.return true to skip, false * otherwise * * @return {boolean} true, on success; throws otherwise * @throws {mw.guidedTour.TourDefinitionError} On invalid input */ mw.log.deprecate( gt, 'defineTour', function ( tourSpec ) { var tourBuilder, stepBuilders = [], steps, i, j, stepCount; /** * Prepares a stepSpec for being passed to firstStep or step * * @private * * @param {number} index 0-based index in step array for step to convert * @param {Object} stepSpec Specification * * @return {Object} Augmented object */ function convertStepSpec( index, stepSpec ) { return $.extend( true, { name: ( index + 1 ).toString(), allowAutomaticNext: false }, stepSpec ); } /** * Follows the chain of shouldSkip through the steps, and returns the * resulting StepBuilder, mw.guidedTour.TransitionAction#hide (if the last * shouldSkip returns true), or undefined * * @private * * @param {number} skipStartIndex 0-based index of the step to start at * * @return {mw.guidedTour.StepBuilder|mw.guidedTour.TransitionAction|undefined} * next step, hide (if the tour should be hidden for now), or undefined * for no change */ function followShouldSkip( skipStartIndex ) { var skipIndex = skipStartIndex; while ( skipIndex < stepCount && steps[ skipIndex ].shouldSkip && steps[ skipIndex ].shouldSkip() ) { skipIndex++; } if ( skipIndex === skipStartIndex ) { // No change, so don't skip return undefined; } else if ( skipIndex < stepCount ) { return stepBuilders[ skipIndex ]; } else { // Skipped past the end return gt.TransitionAction.HIDE; } } /** * Gets a transition callback for the given start index * * @private * * @param {number} startIndex 0-based index of step to convert * * @return {Function} Handler that returns the target after skipping */ function getTransitionHandler( startIndex ) { return function () { return followShouldSkip( startIndex ); }; } if ( arguments.length !== 1 ) { // Object itself is checked in TourBuilder. throw new gt.TourDefinitionError( 'Check your syntax. There must be exactly one argument, \'tourSpec\', which must be an object.' ); } tourBuilder = new gt.TourBuilder( tourSpec ); steps = tourSpec.steps; if ( !Array.isArray( steps ) || steps.length < 1 ) { throw new gt.TourDefinitionError( '\'tourSpec.steps\' must be an array, a list of one or more steps.' ); } stepCount = steps.length; stepBuilders[ 0 ] = tourBuilder.firstStep( convertStepSpec( 0, tourSpec.steps[ 0 ] ) ); for ( i = 1; i < stepCount; i++ ) { stepBuilders[ i ] = tourBuilder.step( convertStepSpec( i, steps[ i ] ) ); } for ( j = 0; j < stepCount; j++ ) { if ( j < stepCount - 1 ) { stepBuilders[ j ].next( stepBuilders[ j + 1 ] ); } // Don't register a custom skip handler if it can never skip. if ( steps[ j ].shouldSkip ) { stepBuilders[ j ].transition( getTransitionHandler( j ) ); } } return true; } ); // Keep after main mw.guidedTour methods. // jsduck assumes methods belong to the classes they follow in source // code order. /** * Error subclass for errors that occur during tour definition * * @class mw.guidedTour.TourDefinitionError * @extends Error */ /** * @constructor * * @param {string} message Error message text */ gt.TourDefinitionError = function ( message ) { this.message = message; }; gt.TourDefinitionError.prototype.toString = function () { return 'TourDefinitionError: ' + this.message; }; gt.TourDefinitionError.prototype.constructor = gt.TourDefinitionError; /** * Error subclass for invalid arguments (that are not part of tour definition) * * @class mw.guidedTour.IllegalArgumentError * @extends Error */ /** * @constructor * * @param {string} message Error message text */ gt.IllegalArgumentError = function ( message ) { this.message = message; }; gt.IllegalArgumentError.prototype.toString = function () { return 'IllegalArgumentError: ' + this.message; }; gt.IllegalArgumentError.prototype.constructor = gt.IllegalArgumentError; initialize(); }( mw.libs.guiders ) );