User:Evad37/hotport/sandbox.js
Appearance
< User:Evad37 | hotport
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Evad37/hotport/sandbox. |
/***************************************************************************************************
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>