/***************************************************************************************************
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>
User:Evad37/hotport/sandbox.js
Audio Reader
12 min
Listen to Narration
AI synthesized audio
0:00
~12:00
Speed:
Wikimedia Attribution & Licensing
Text syndicated from Wikipedia under the Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0). Original content and full editorial history authored by Wikipedia contributors at https://en.wikipedia.org/wiki/User:Evad37/hotport/sandbox.js.
Images, media assets, and audiovisual material are hosted by Wikimedia Commons and credited to their respective creators under open licensing terms.