User:Evad37/hotport/sandbox.js

/*************************************************************************************************** HotPort --- by Evad37 > Helps add and modify portal links. // /* ========== Config ============================================================================ */ // A global object that stores all the page and user configuration and settings var config = { script: { advert: ' (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 = { // (and redirects). Portals are in positional parameters 1, 2, 3, ... 'portal': [ 'portal', 'portal box', 'ports', 'portal-2' ],	// (and redirects). Portals are in positional parameters 1, 2, 3, ... 'bar': [ 'portal bar', 'portalbar' ],	// (and redirects). Portal in positional parameter 1. 'inline': [ 'portal-inline', 'portal-inline-template', 'portal inline', 'portal frameless' ],	// . 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. *  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($(' ').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: '' } ),					new OO.ui.MenuOptionWidget( { data: 'Portal-inline', label: '' } ),					new OO.ui.MenuOptionWidget( { data: 'Portal bar', label: '' } )				]			}		}),		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 = ' (HotPort/sandbox)'; var FAKEAPI_FAIL_RATE = 0; // number between 0 (never fail) and 1 (always fail) $(' ')		.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 ) //