User:Evad37/hotport/sandbox.js

Source: Wikipedia, the free encyclopedia.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/***************************************************************************************************
 HotPort --- by Evad37
 > Helps add and modify portal links.
***************************************************************************************************/
// <nowiki>
/* ========== Config ============================================================================ */
// A global object that stores all the page and user configuration and settings
var config = {
	script: {
		advert:  ' ([[User:Evad37/hotport.js|HotPort]])',
		version: '0.0.1-alpha'
	},
	// MediaWiki configuration values
	mw: mw.config.get([
		'wgPageName',
		'wgNamespaceNumber'
	]),
	moduleDependenciesLoaded: mw.loader.using( [
		'mediawiki.util',
		'mediawiki.api',
		'mediawiki.Title',
		'mediawiki.RegExp',
		'oojs-ui-core',
		'oojs-ui-widgets',
		'oojs-ui-windows',
		'jquery.ui'
	] ),
	scriptDependenciesLoaded: $.when(
		Window.parseAllTemplates || $.getScript('https://en.wikipedia.org/w/index.php?title=User:SD0001/parseAllTemplates.js&action=raw&ctype=text/javascript'),
	),
	scriptShouldRun: (function() {
		var deferred = $.Deferred();
		var conf = mw.config.get([
			'wgPageName',
			'wgNamespaceNumber',
			'wgIsMainPage',
			'wgUserName',
			'wgAction',
			'wgDiffOldId'
		]);
		var isTesting = conf.wgPageName.includes('User:Evad37/hotport/') &&
			conf.wgAction === 'view' &&
			conf.wgDiffOldId === null;
		if ( isTesting ) {
			deferred.resolve('testing');
		} else {
			var notInViewMode = /(?:\?|&)(?:action|diff|oldid)=/.test(window.location.href);
			// correct namespaces are 0 (main), 2 (user) 14 (category)
			var notInCorrectNamespace = ![0, 2, 14].includes(conf.wgNamespaceNumber);
			var isBaseUserpage = config.mw.wgNamespaceNumber === 2 &&
				!cconf.wgPageName.includes('/');
			var isSomeoneElsesPage = config.mw.wgNamespaceNumber === 2 &&
				conf.wgPageName.indexOf('User:' + conf.wgUserName) !== 0;
			var pageDoesNotExist = $('li.new[id|=ca-nstab]').length > 0;
			var isMainPage = conf.wgIsMainPage;
			if (
				notInViewMode || notInCorrectNamespace || isBaseUserpage ||
				isSomeoneElsesPage || pageDoesNotExist || isMainPage
			) {
				deferred.reject();
			} else {
				deferred.resolve('normal');
			}
		}
		return deferred.promise();
	})()
};

$.when(
	config.scriptShouldRun,
	config.moduleDependenciesLoaded,
	config.scriptDependenciesLoaded,
	$.ready
).then(function(scriptMode) {

/* ========== Wikitext Analysis ================================================================== */
// TemplateParser from meta-script [[User:SD0001/parseAllTemplates.js]]
// TODO: Get the entrie wikitext of the transclusion. Should probably be done in meta-script? Or
// perhaps fork a version, and use a similar format to RATER script?


/**
 * 
 * @param {mw.Api} api 
 * @param {String} pageName 
 * @returns {Promise(String)} page wikitext
 */
var getPageWikitext = function(api, pageName) {
	return api.get({
		action: 'query',
		titles: pageName,
		prop: 'revisions',
		rvprop: 'content|timestamp',
		indexpageids: 1
	} )
	.then( function(response) {
		var id = response.query.pageids;
		return response.query.pages[ id ].revisions[ 0 ];
	});
};

var portalLinkTemplates = {
	// {{Portal}} (and redirects). Portals are in positional parameters 1, 2, 3, ...
	'portal': [
		'portal',
		'portal box',
		'ports',
		'portal-2'
	],
	// {{Portal bar}} (and redirects). Portals are in positional parameters 1, 2, 3, ...
	'bar': [
		'portal bar',
		'portalbar'
	],
	// {{Portal-inline}} (and redirects). Portal in positional parameter 1.
	'inline': [
		'portal-inline',
		'portal-inline-template',
		'portal inline',
		'portal frameless'
	],
	// {{Subject bar}}. Portals are in parameters |portal=, |portal1=, |portal2=, |portal3=, ...
	'subject': [
		'subject bar'
	]
};
var portalLinkTemplatePattern = /(?:[Pp]orts|[Pp]ortal|Subject)[ _\-]?(?:box|bar|\-2|inline(?:\-template)?|frameless)?/;

/**
 * 
 * @param {String} pageName
 * @returns {String} normalised page name  
 */
var normalisePageName = function(pageName) {
	var title = mw.Title.newFromText(pageName);
	return title && title.getPrefixedText() || pageName;
};

/**
 * 
 * @param {String} pageWikitext
 * @returns {TemplateObject[]} portal link templates on page
 */
var findPortalLinkTemplates = function(pageWikitext) {
	return Window.parseAllTemplates(pageWikitext)
	.filter(function(template) {
		return portalLinkTemplatePattern.test( normalisePageName( template[0] ) );
	});
};

/**
 * 
 * @param {String|Number} name Parameter name (or position number of unnamed parameter)
 * @returns {Boolean}
 */
var isPositionalParameter = function(name) {
	return name !== 0 && ( Number.isInteger(name) || /^\d+$/.test(name) );
};

/**
 * 
 * @param {String|Number} name Parameter name (or position number of unnamed parameter)
 * @returns {Boolean}
 */
var isPositionalParameterOne = function(name) {
	return name === 1 || name === '1';
};

/**
 * 
 * @param {String|Number} name Parameter name (or position number of unnamed parameter)
 * @returns {Boolean}
 */
var isNumberedPortalParameter = function(name) {
	return /^portal\d+$/.test(name);
};

/**
 * 
 * @param {Object} parameters key-value pairs of parameter names and values 
 * @param {String} templateName
 * @returns {String[]} names of portals used in templates
 */
var findPortals = function(parameters, templateName) {
	var template = normalisePageName(templateName).toLowerCase();
	var usesPositionalParams = portalLinkTemplates.portal.includes(template) ||
		portalLinkTemplates.bar.includes(template);
	var usesPositionalParameterOne = portalLinkTemplates.inline.includes(template);
	var usesNumberedPortalParameters = portalLinkTemplates.subject.includes(template);
	return $.map(parameters, function(value, name) {
		// Positional parameter
		if ( usesPositionalParams && isPositionalParameter(name) ) {
			return value;
		}
		if ( usesPositionalParameterOne && isPositionalParameterOne(name) ) {
			return value;
		}
		if ( usesNumberedPortalParameters && isNumberedPortalParameter(name) ) {
			return value;
		}
		return null;
	} );
};

/**
 * 
 * @param {TemplateObject[]} templates 
 * @returns {Object} Info object of form { templates: TemplateObject[], portalNames: String[] }
 */
var reducePortalNames = function(templates) {
	var portalNames = templates.map(findPortals).reduce(function(prev, cur) {
		return prev.concat(cur);
	});
	return {
		tempaltes: templates,
		portalNames: portalNames
	};
};

/**
 * Takes in wikitext of a page. Returns:
 * - a Template object for the portal-link template, if found
 * - an array of linked portal page names, if any were found in the template (e.g. {{subject bar}}
 *  might not have any portals specified)
 * - the section number to be edited. Either the section containing an existing portal template, or
 * a "See also" section, or the last section
 * @param {mw.Api} api 
 * @param {String} pageName
 * @returns {Promise} resolved with an info object: {templates: templateObject[], portalNames: String[]}
 */
var analysePageWikitext = function(api, pageName) {
	return getPageWikitext(api, pageName)
	.then(findPortalLinkTemplates)
	.then(reducePortalNames);
}


/* ========== Overlay dialog ==================================================================== */
// TODO: For use by show preview / show changes

/* ========== Custom OOUI widgets =============================================================== */

/**
 * @class PortalLookupInputWidget
 * @description A text input with an updating menu of options of portals which match the value
 * of the text input.
 * @inheritdoc https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/OO.ui.TextInputWidget
 * @mixin OO.ui.mixin.LookupElement 
 * @param {Object} config Configuration options
 * @param {mw.API} config.api API Object (Required!)
 * @method getLookupRequest() Looks up data (portal page names) from the value of the input
 * @method getLookupCacheDataFromResponse() Overrides method from parent constructor
 * @method getLookupMenuOptionsFromData() Maps data (portal page names) into menu options
 */
var PortalLookupInputWidget = function PortalLookupInputWidget( config ) {
	// Parent constructor
	OO.ui.TextInputWidget.call( this, $.extend( { validate: /^[^\[\]\{\}\|\#]+$/ }, config ) );
	// Mixin constructors
	OO.ui.mixin.LookupElement.call( this, config );
	
	this.api = config.api;
};
OO.inheritClass( PortalLookupInputWidget, OO.ui.TextInputWidget );
OO.mixinClass( PortalLookupInputWidget, OO.ui.mixin.LookupElement );


PortalLookupInputWidget.prototype.getLookupRequest = function () {
	var	value = this.getValue();
	var deferred = $.Deferred();
	var api = this.api;

	this.getValidity().then( function () {
		api.get({
			action: 'query',
			format: 'json',
			list: 'prefixsearch',
			pssearch: value,
			psnamespace: 100,
			pslimit: 10
		})
		.then(function(response) {
			deferred.resolve(response);
		});
	}, function () {
		// No results when the input contains invalid content
		deferred.resolve( [] );
	} );

	return deferred.promise( { abort: function () {} } );
};

PortalLookupInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) {
	return response || [];
};

PortalLookupInputWidget.prototype.getLookupMenuOptionsFromData = function ( data ) {
	return data.query.prefixsearch.map(function(searchresult) {
		return new OO.ui.MenuOptionWidget( {
			data: searchresult.title.replace('Portal:',''),
			label: searchresult.title.replace('Portal:','')
		} );
	});
};

/**
 * @class PortalSelectionWidget
 * @description A PortalLookupInputWidget, plus Okay and Cancel buttons, plus
 * {@todo} redlink/existance detection.
 * @inheritdoc https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/OO.ui.TextInputWidget
 * @mixin OO.ui.mixin.LookupElement 
 * @param {Object} config Configuration options
 * @param {mw.API} config.api API Object (Required!)
 * @emits #confirm User confirmed value in the input {@property {String} value of input}
 * @emits #cancel Cancelled by user
 */
var PortalSelectionWidget = function PortalSelectionWidget(config) {
	config = config || {};
	// Parent constructor
	PortalSelectionWidget.parent.call( this, config );

	this.input = new PortalLookupInputWidget(config);
    this.okayButton = new OO.ui.ButtonWidget( {
        icon: 'check',
		title: 'Okay'
    } );
    this.cancelButton = new OO.ui.ButtonWidget( {
        icon: 'close',
		title: 'Cancel',
        framed: false
    } );

    this.layout = new OO.ui.HorizontalLayout({
		items: [
            this.input,
            this.okayButton,
            this.cancelButton
        ]
    });
    this.$element.append(this.layout.$element);
    
    // Connect events to handlers
    this.input.connect( this, { enter: 'onOkayButtonClick' } );
	this.okayButton.connect( this, { click: 'onOkayButtonClick' } );
	this.cancelButton.connect( this, { click: 'onCancelButtonClick' } );
};
OO.inheritClass( PortalSelectionWidget, OO.ui.TextInputWidget );

// Handlers re-emit events
PortalSelectionWidget.prototype.onOkayButtonClick = function() {
    this.emit('confirm', this.input.getValue());
};
PortalSelectionWidget.prototype.onCancelButtonClick = function() {
    this.emit('cancel');
};

/**
 * @class PortalListingWidget
 * @description Represents a portal link (either already on page, or to be added). Has a label
 * linking to the portal, buttons to edit or remove, and a portal selector (hidden until needed) 
 * @inheritdoc https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/OO.ui.Widget
 * @param {Object} config Configuration options
 * @param {mw.API} config.api API Object (Required!)
 * @param {Object} config.data Portal data (Required!) 
 * @param {String} config.data.name Portal page name, without the "Portal:" prefix (Required!)
 * @method setRedlinkStatusFromAPI() Queries api for page existance, and sets redlink styling
 * if page does not exist
 * @method onEditButtonClick() Hides label and buttons, shows the portal selector
 * @method onRemoveButtonClick() Marks portal link for removal
 * @emits #remove User marked portal link for removal
 * @emits #modify User modified the portal link
 */
var PortalListingWidget = function PortalListingWidget(config) {
	// Call parent constructor
    PortalListingWidget.parent.call( this, config );
    
    this.api = config.api;
	
	// Component widgets
	this.labelButton = new OO.ui.ButtonWidget( {
        label: config.data.name,
        framed: false,
		title: 'Open portal',
        classes: ['hotport-label'],
        href: 'https://en.wikipedia.org/wiki/Portal:' + mw.util.wikiUrlencode(config.data.name)
    } );
    this.editButton = new OO.ui.ButtonWidget( {
        icon: 'edit',
		title: 'Edit',
        framed: false
    } );
    this.removeButton = new OO.ui.ButtonWidget( {
        icon: 'clear',
		title: 'Remove',
        framed: false
    } );
    this.buttonGroup = new OO.ui.ButtonGroupWidget( {
		items: [ labelButton, editButton, removeButton ]
	} );
    this.portalSelector = new PortalSelectionWidget({ value: config.data.name });

	this.$element.append(
        this.buttonGroup.$element,
        portalSelector.$element
	);
	
	// Connect events to handlers
	this.editButton.connect( this, { click: 'onEditButtonClick' } );
    this.removeButton.connect( this, { click: 'onRemoveButtonClick' } );
    this.portalSelector.connect( this, {
        confirm: 'onSelectorConfirm',
        cancel: 'onSelectorCancel'
    } );

};
OO.inheritClass( PortalListingWidget, OO.ui.Widget );

PortalListingWidget.prototype.setRedlinkStatusFromAPI = function() {
    var labelButton = this.labelButton;
    var page = labelButton.getLabel();
    var redlinkPromise = this.api.get({
        action: "query",
        format: "json",
        titles: page,
        indexpageids: 1,
    })
    .then(function(response) {
        return response.query.pageids[0] < 0;
    });

    redlinkPromise.then(function(isRedlink) {
        if ( isRedlink ) {
            labelButton.setFlags('destructive');
        }
    });
};

// Handlers for events, including emitting some events
PortalListingWidget.prototype.onEditButtonClick = function () {
    // Hide buttons, show portal selector
    this.buttonGroup.toggle(false);
    this.portalSelector.toggle(true);

};
PortalListingWidget.prototype.onRemoveButtonClick = function () {
    // Strike-through the label
    var portal = this.labelButton.getLabel();
    this.labelButton.setLabel($('<span>').css({'text-decoration': 'line-through'}).text(portal));
    // Set disbaled state -- makes it easy to tell this portal has been removed using .isDisabled() method,
    // and gives it a nice grey colour
    this.labelButton.setDisabled(true);
    // Hide the remove button, since it is now marked for removal
    this.removeButton.toggle(false);
    // Emit a remove event
	this.emit('remove');
};
PortalListingWidget.prototype.onSelectorConfirm = function (selectedPortal) {
    // Show buttons, hide portal selector
    this.buttonGroup.toggle(true);
    this.removeButton.toggle(true);
    this.portalSelector.toggle(false);
    // Reset the label button
    this.labelButton.setLabel(selectedPortal);
    this.labelButton.setHref('https://en.wikipedia.org/wiki/Portal:' + mw.util.wikiUrlencode(selectedPortal));
    this.labelButton.setFlags({'destructive': false});
    this.labelButton.setDisabled(false);
    this.setRedlinkStatusFromAPI();
    // Emit a modify event
    this.emit('modify');
};
PortalListingWidget.prototype.onSelectorCancel = function () {
    // Show buttons, hide and clear portal selector
    this.buttonGroup.toggle(true);
    this.portalSelector.toggle(false);
    this.portalSelector.setValue('');
};

////////////////////////////////////////////////////////////////////////////////////////////////////
// TODO: Windows and WindowManager(s?) for param editing, show preview, show changes              //
////////////////////////////////////////////////////////////////////////////////////////////////////

/* ========== HotPortBar ======================================================================== */
/**
 * @class HotPortBarWidget
 * @description The user-interface bar.
 * @inheritdoc https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/OO.ui.Widget
 * @param {Object} config Configuration info.
 * @param {String} config.pageName Page name.
 * @param {String} config.wikitext Page wikitext.
 * @param {Template|undefined} config.template Template currently used on page.
 * @param {String[]|undefined} config.portals Names of portals already linked to on page.
 * @param {mw.API} config.api API Object
 * @method onParamtersButtonClick() // TODO 
 * @method onAddButtonClick() 
 * @method onPortalAdded() 
 * @method onPortalSelectionCancelled() 
 * @method onSaveButtonClick() 
 * @method onPreviewButtonClick() 
 * @method onChangesButtonClick() 
 * @method onPortalModify() // TODO: Is this needed? Can it just be handled by the listing widget?
 * @method onPortalRemove() // TODO: Same for this one?
 */
var HotPortBarWidget = function HotPortBarWidget(config) {
	// Config for parent constructor
	var parentConfig = {
		classes: ['hotport']
	};
	// Call parent constructor
	HotPortBarWidget.parent.call( this, parentConfig );
	
	// Create widgets
	this.widgets = {
		titleLabel: new OO.ui.LabelWidget({label: 'Portals'}),
		
		templateDropdown: new OO.ui.DropdownWidget( {
			label: 'Dropdown menu: Select a menu option',
			menu: {
				items: [
					new OO.ui.MenuOptionWidget( {
						data: 'Portal',
						label: '{{Portal}}'
					} ),
					new OO.ui.MenuOptionWidget( {
						data: 'Portal-inline',
						label: '{{Portal-inline}}'
					} ),
					new OO.ui.MenuOptionWidget( {
						data: 'Portal bar',
						label: '{{Portal bar}}'
					} )
				]
			}
		}),
		
		paramtersButton: new OO.ui.ButtonWidget({
			icon: 'puzzle',
			title: 'Edit optional parameters'
		}),
		
		portalsList: new OO.ui.HorizontalLayout({
			items: (config.portals||[]).map(function(portal) {
				return new PortalListingWidget({
                    data: {
                        name: portal
                    }
				});
			})
		}),
		
		addButton: new OO.ui.ButtonWidget({
			icon: 'add',
			title: 'Add a portal'
        }),
        
        newPortalSelection: new PortalSelectionWidget(),

		saveButton: new OO.ui.ButtonWidget({
			label: 'Save',
			title: 'Save changes',
			flags: ['progressive']
		}),

		previewButton: new OO.ui.ButtonWidget({
			label: 'Show preview',
			title: 'Show preview of changes'
		}),

		changesButton: new OO.ui.ButtonWidget({
			label: 'Show changes',
			title: 'Show changes to the wikitext'
		})
	};
	
    // Toggle off things not needed initially
    this.widgets.newPortalSelection.toggle(false);
	this.widgets.saveButton.toggle(false);
	this.widgets.previewButton.toggle(false);
	this.widgets.changesButton.toggle(false);
	
	// Add layout containing the widgets
	this.layout = new OO.ui.HorizontalLayout({
		items: [
			this.widgets.titleLabel,
			this.widgets.templateDropdown,
			this.widgets.paramtersButton,
			this.widgets.portalsList,
            this.widgets.addButton,
            this.widgets.newPortalSelection,
			this.widgets.saveButton,
			this.widgets.previewButton,
			this.widgets.changesButton
		]
	});
	this.$element.append(this.layout.$element);

	// Aggregate events from the portalsList
	this.widgets.portalsList.aggregate({
		modify: 'portalModify',
		remove: 'portalRemove'
	});
	
	// Connect widget events to handlers
	this.widgets.paramtersButton.connect( this, { click: 'onParamtersButtonClick' } );
    this.widgets.addButton.connect( this, { click: 'onAddButtonClick' } );
    this.widgets.newPortalSelection.connect( this, {
        confirm: 'onPortalAdded',
        cancel: 'onPortalSelectionCancelled'
    } );
	this.widgets.saveButton.connect( this, { click: 'onSaveButtonClick' } );
	this.widgets.previewButton.connect( this, { click: 'onPreviewButtonClick' } );
	this.widgets.changesButton.connect( this, { click: 'onChangesButtonClick' } );
	this.widgets.portalsList.connect( this, { portalModify: 'onPortalModify' });
	this.widgets.portalsList.connect( this, { portalRemove: 'onPortalRemove' });


};
OO.inheritClass( HotPortBarWidget, OO.ui.Widget );

HotPortBarWidget.prototype.onParamtersButtonClick = function() {
    editParametersDialog.open();
    var self = this;
    var data = this.getData();
    editParametersDialog.open()
    .then(function(parameters) {
    })
};

HotPortBarWidget.prototype.onAddButtonClick = function () {
    // Show new portal selection, hide the Add button 
    this.widgets.newPortalSelection.toggle(true);
    this.widgets.addButton.toggle(false);
};

HotPortBarWidget.prototype.onPortalAdded = function (portal) {
    // Hide new portal selection, show the Add button, add a new portal to the list 
    this.widgets.newPortalSelection.toggle(false);
    this.widgets.addButton.toggle(true);
    this.widgets.portalsList.additems([
        new PortalListingWidget({
            data: {
                name: portal,
                preexisitng: false,
                modified: true
            }
        })
    ]);
};

HotPortBarWidget.prototype.onPortalSelectionCancelled = function () {
    // Hide new portal selection, show the Add button 
    this.widgets.newPortalSelection.toggle(false);
    this.widgets.addButton.toggle(true);
};


//var exisitngPortalLinks = $('#mw-content-text').find("a[title^='Portal:']");
//var navboxPortalLinks = $('#mw-content-text').find('div.navbox').find('a[title^="Portal:"]');

/* ========== Get Started ======================================================================= */

if ( scriptMode === 'normal' ) {
	var api = new mw.Api({ ajax: { headers: {
        'Api-User-Agent': 'HotPort/' + config.script.version + 
        ' ( https://en.wikipedia.org/wiki/User:Evad37/hotport )'
	} } });

	analysePageWikitext(api, config.mw.wgPageName)
    .then(function(templates, portalNames) {
		//TODO: Should also have the page wikitext available
        var barConfig = {/*TODO*/};
        mw.util.$content.append(
            new HotPortBarWidget(barConfig).$element
        );
    });
}

/* ========== Testing (remove when updating main script) ======================================== */
else {
	config.version += '/sandbox';
	config.ad = ' ([[User:Evad37/hotport/sandbox.js|HotPort/sandbox]])';
	var FAKEAPI_FAIL_RATE = 0; // number between 0 (never fail) and 1 (always fail)
	$('<div>')
		.attr('id', 'qunit')
		.insertBefore('#firstHeading');
	var FakeApi = function() {
		this.realApi = new mw.Api({ ajax: { headers: {
			'Api-User-Agent': 'HotPort/' + config.script.version + 
			' ( https://en.wikipedia.org/wiki/User:Evad37/hotport )'
		} } });
	};
	FakeApi.prototype.get = function(query) {
		return this.realApi.get(query);
	};
	FakeApi.prototype.postWithToken = function(token, params) {
		console.log(params);
		if ( Math.random() < FAKEAPI_FAIL_RATE ) {
			return $.Deferred().reject('Random error');
		}
		var response = {};
		response[params.action] = { result: 'Success' };
		return $.Deferred().resolve(response);
	};
	
	var QUnitLoaded = (function() {
		mw.loader.load('https://en.wikipedia.org/w/index.php?title=User:Evad37/qunit-2.8.0.css&action=raw&ctype=text/css', 'text/css');
		return $.getScript('https://en.wikipedia.org/w/index.php?title=User:Evad37/qunit-2.8.0.js&action=raw&ctype=text/javascript');
	})();

	$.when(QUnitLoaded).then(function() {
			QUnit.module('module name goes here');
			QUnit.test('test name goes here', function(assert) {
				var actual1 = true;
				var actual2 = {'result': 'foo'};
				var expected2 = {'result': 'foo'};

				assert.ok(actual1,
					'assertion name goes here for `ok` assertion');
				assert.deepEqual(actual2, expected2,
					'assertion name goes here for `deep equal` assertion');
			});
	});// end of when( QUnitLoaded )
} // end of testing

});//end of when( scriptShouldRun, dependenciesLoaded, ready )
//</nowiki>