MediaWiki:Guidedtour-lib.js
Jump to navigation
Jump to search
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* Library to easily create and maintain translatable guided tours.
*
* Important note: This script must be loaded before the entire page is ready because
* otherwise GuidedTours will try to start a tour although it is not defined yet.
*
* @author Bene*
*
* Temporary note:
* This version has some temporary fixes that will be resolved more thoroughly very
* shortly. In the meantime, use `actionBtn2` instead of `actionBtn` in step options
* if you want a click action to be completed automatically for a step.
*/
( function( $, mw, gt, wb ) {
'use strict';
$.ajax( {
async: false,
url: mw.util.wikiScript() + '?title=MediaWiki:JQuery.overlay.js&action=raw&ctype=text/javascript',
dataType: 'script'
} );
mw.util.addCSS(
'.tipsy { z-index: 100000006; } /* tipsy should stay visible */ \
.ui-suggester-list.ui-ooMenu { z-index: 1000000061; } /* suggestions should stay visible */ \
#mw-spinner-guidedtour { position: absolute; height: 100%; background-color: rgba(0, 0, 0, 0.1); z-index: 1000000; } /* the spinner */ \
#bodyContent { z-index: auto; } /* Ugly hack to make the overlay work again */'
);
var defaultStep = {
overlay: false,
closeOnClickOutside: false,
position: 'bottom', // only when attached
actionBtn: '',
actionBtn2: '', // Hack! Using separate property to prevent default click on action btn triggered in guiders library
actionComplete: false,
xButton: false
};
/**
* Shows a spinner covering the whole page.
*/
function showSpinner() {
$( 'body' ).css( 'overflow', 'hidden' );
mw.loader.using( [ 'jquery.spinner' ], function() {
$.createSpinner( { size: 'large', type: 'block', id: 'guidedtour' } ).prependTo( 'body' );
} );
}
/**
* Removes the data from the current entity and adds the new data.
* When finished reloads the site with data=ok.
*
* @param {string} tourName
* @param {object} newData
*/
function removeData( tourName, newData ) {
mw.loader.using( 'mediawiki.api', function() {
new mw.Api()
.postWithEditToken( {
action: 'wbeditentity',
id: mw.config.get( 'wbEntityId' ),
clear: true,
data: JSON.stringify( newData ),
summary: 'Clearing data for [[Wikidata:Tours#' + tourName + '|' + tourName + ']] tour'
} )
.done( function() {
location.href += '&data=ok';
} );
} );
}
/**
* Parses the given page and returns the sections used for the single steps of the tour.
*
* @param {string} pageName
* @param {string} language
* @return {array}
*/
function getSections( pageName, language ) {
var data = JSON.parse( $.ajax( {
dataType: 'json',
url: '/w/api.php', // this is ugly but necessary if we want to execute the tour on testwikidata
data: {
'action': 'parse',
'format': 'json',
'page': pageName + ( language === 'en' ? '' : '/' + language ),
'prop': 'text|sections',
'disablepp': true,
'disabletoc': true
},
async: false
} ).responseText );
if ( data.error ) {
if ( language !== 'en' ) {
location.href += '&uselang=en';
} else {
console.log( data.error );
return false;
}
}
var rawSections = data.parse.text['*'].split( /<h2[^>]*>((?!<\/h2>).)*<\/h2>/gi ),
titles = data.parse.sections,
sections = [];
for ( var i = 0; i < titles.length; i++ ) {
sections.push( {
title: titles[i].line,
// Make sure this is never undefined (T364882)
description: rawSections[i * 2 + 2] || ''
} );
}
return sections;
}
/**
* Builds the tour's options with steps parsed form the given page.
*
* @param {string} tourName
* @param {string} tourEntityId
* @param {object} options
*
* @return {object}
*/
function buildOptionsFromPage( tourName, tourEntityId, options ) {
var language = mw.config.get( 'wgUserLanguage' ),
sections = getSections( options.pageName, language );
$.extend( options, {
name: tourName,
shouldLog: true
} );
$.each( sections, function( i ) {
var step = options.steps[i] = $.extend( {}, defaultStep, options.steps[i], sections[i] ),
_onShow = step.onShow;
$.extend( options.steps[i], {
onShow: function() {
if ( typeof _onShow === 'function' ) {
_onShow();
}
if (step.actionComplete) {
// Action already completed
// Hack! Re-add the the click handler for moving to next slide.
// The default click gets removed when mutation observer is added, so we can wait for DOM before triggering next step
$('.guidedtour-next-button').off('click').on('click', function() {
mw.libs.guiders.next();
});
} else if ( step.actionBtn2 ) {
var nextStep = options.steps[i+1];
// Hack! Intercept the click to prevent going to next slide before DOM is ready.
// Instead, trigger the action button click and let the mutation observer detect the change and advance the slide
// Proper fix requires small changes to guiders library
$('.guidedtour-next-button').off('click').one('click', function() {
$(step.actionBtn2).click();
});
if( nextStep && nextStep.attachTo && $(nextStep.attachTo).length === 0) {
// The attach element for next step is not in the DOM yet, add mutation observer
step.observer = newElementObserver(nextStep.attachTo, function() {
step.observer.disconnect();
step.actionComplete = true;
return mw.libs.guiders.next();
})
} else {
// step has an action button, but there is no attach in the next slide or it's element is already visible
// add the click event to advance the slide from an action button click, as there are no mutations to trigger it
$(step.actionBtn2).one('click', function () {
step.actionComplete = true;
mw.libs.guiders.next();
})
}
}
if ( step.overlay ) {
console.log($( step.overlay ));
$( step.overlay ).overlay();
} else {
$.overlay.remove();
}
}
} );
// change the defaults for the end of the tour
if ( i === sections.length - 1 ) {
$.extend( options.steps[i], {
overlay: false,
//closeOnClickOutside: true,
buttons: [ { action: 'end' } ]
} );
}
} );
if ( location.href.indexOf( 'uselang=en' ) > -1 ) {
options.steps.unshift( $.extend( {}, defaultStep, {
title: 'Not your language',
description: 'This tour is not available in your language. You may proceed using English or ' +
'<a href="' + mw.util.getUrl( options.pageName ) + '">translate it into your language</a>.'
} ) );
}
return options;
}
/**
* Defines the tour with the given options using the TourBuilder.
*
* @param {object} options
*/
function defineTour( options ) {
var tour = new gt.TourBuilder( {
name: options.name,
shouldLog: true
} );
tour.firstStep( $.extend( {
name: 'step0'
}, options.steps[0] ) ).next( 'step1' );
for ( var i = 1; i < options.steps.length - 1; i++ ) {
tour.step( $.extend( {
name: 'step' + i
}, options.steps[i] ) ).next( 'step' + ( i + 1 ) ).back( 'step' + ( i - 1 ) );
}
tour.step( $.extend( {
name: 'step' + ( options.steps.length - 1 )
}, options.steps[options.steps.length - 1] ) ).back( 'step' + ( options.steps.length - 2 ) );
}
/**
* Creates mutation observer which run the callback when an element is added
* that matches the selector
*
* @param {string} selector
* @param {function} callback
*
* @return {object}
*/
function newElementObserver(selector, callback) {
var observer = new MutationObserver(function (mutationList) {
for(var mutation of mutationList) {
for(var node of mutation.addedNodes) {
if($(node).is(selector) ) {
// The node matching the selector has been added
if (callback) callback();
return;
}
}
}
});
var targetNode = $("body").get(0);
var observerOptions = {
childList: true,
subtree: true
}
observer.observe(targetNode, observerOptions);
return observer;
}
/**
* Checks if the tour name and entity id are correct. Also checks if the data
* has been prepared and clears or adds missing data. If everything is ok,
* starts the tour.
*/
gt.init = function( tourName, tourEntityId, newData, options ) {
// check for the correct page
if ( !wb || location.href.indexOf( 'tour=' + tourName ) < 0 ||
mw.config.get( 'wbEntityId' ) !== tourEntityId || mw.config.get( 'wbIsEditView' ) !== true
) {
return;
}
// check for emptiness
if ( !gt.hasQuery( { 'data' : 'ok' } ) ) {
showSpinner();
removeData( tourName, newData );
return;
}
// launch the tour
defineTour( buildOptionsFromPage( tourName, tourEntityId, options ) );
};
} )( jQuery, mediaWiki, mediaWiki.guidedTour, wikibase );