משתמש:Bharel/סקריפטים/64-new.js
הערה: לאחר הפרסום, ייתכן שיהיה צורך לנקות את זיכרון המטמון (cache) של הדפדפן כדי להבחין בשינויים.
- פיירפוקס / ספארי: להחזיק את המקש Shift בעת לחיצה על טעינה מחדש (Reload) או ללחוץ על צירוף המקשים Ctrl-F5 או Ctrl-R (במחשב מק: ⌘-R).
- גוגל כרום: ללחוץ על צירוף המקשים Ctrl-Shift-R (במחשב מק: ⌘-Shift-R).
- אינטרנט אקספלורר / אדג': להחזיק את המקש Ctrl בעת לחיצה על רענן (Refresh) או ללחוץ על צירוף המקשים Ctrl-F5.
- אופרה: ללחוץ על Ctrl-F5.
/* Rewritten and refactored by Bar Harel (User:Bharel) based on old 64.js */
/* globals importStylesheet, mw, $, confirm, prompt, console, alert */
importStylesheet('משתמש:Bharel/סקריפטים/64-new.css');
const script64Manager = function(){
"use strict";
const Manager = {};
const SettingTypes = Manager.SettingTypes = {
NORMAL: "NORMAL",
ANONYMOUS: "ANONYMOUS",
GENERAL: "GENERAL"
};
const SettingPage = {
[SettingTypes.NORMAL]:
('משתמש:' + mw.config.get('wgUserName') +
'/סקריפט 64/UserSettings.js'),
[SettingTypes.ANONYMOUS]:
('משתמש:' + mw.config.get('wgUserName') +
'/סקריפט 64/AnonSettings.js'),
[SettingTypes.GENERAL]:
('משתמש:' + mw.config.get('wgUserName') +
'/סקריפט 64/GeneralSettings.js'),
};
const DefaultSettingPage = {
[SettingTypes.NORMAL]: 'משתמש:Bharel/סקריפט 64/DefaultUserSettings.js',
[SettingTypes.ANONYMOUS]: 'משתמש:Bharel/סקריפט 64/DefaultAnonSettings.js',
[SettingTypes.GENERAL]:
'משתמש:Bharel/סקריפט 64/DefaultGeneralSettings.js',
};
const ALL_TEMPLATES_PAGE = 'משתמש:Bharel/סקריפט 64/AllTemplates.js';
const LOCAL_STORAGE_PREFIX = mw.config.get("wgUserName") + "Script64";
const settingsCache = {};
// The manager will call these handlers with itself as first argument
// until one of the handlers returns a true value.
Manager.handlers = [];
function purgeCache_(settingType){
window.localStorage.removeItem(LOCAL_STORAGE_PREFIX+settingType);
delete settingsCache[settingType];
}
/* Gets the settings for the current user
Args:
settingType: Which setting should we get out of SettingTypes.
Returns:
A jQuery ajax promise, which, if successful,
calls .done with settings as a JSON.
*/
function getSettings_(settingType) {
// Fetch from site
return $.ajax({
url: mw.util.wikiScript(),
data: {
action: 'raw',
title: SettingPage[settingType]
},
dataType: 'json'
});
}
/* Sets the settings for the current user
Also, if successful, resets the settings cache.
Args:
anonymous: Should we set the anonymous settings or not
settings: JSON with the settings
Returns:
A jQuery ajax promise.
*/
Manager.setSettings = function(settingType, settings) {
let api = new mw.Api({ajax: {dataType: 'json'}});
return api
.postWithToken('csrf', {
action: 'edit',
title: SettingPage[settingType],
summary: 'Script 64 - Set settings',
text: JSON.stringify(settings),
})
.done(() =>{
// Remove from storage and cache
purgeCache_(settingType);
});
};
/* Gets the settings for the current user.
Unlike getSettings_, this sets and returns the default settings if current
user's settings do not exist.
Args:
anonymous: Should we get the anonymous settings or not.
Returns:
A Promise object that upon .then, calls with the settings.
*/
Manager.getSettings = function(settingType) {
return new Promise((resolve, reject) => {
// Check if already exists in cache
if (settingsCache[settingType])
return resolve(settingsCache[settingType]);
// Check if already exists in storage
const localItem =
window.localStorage.getItem(LOCAL_STORAGE_PREFIX+settingType);
if (localItem){
// Set the settings from storage into the cache and resolve
return resolve(settingsCache[settingType] = JSON.parse(localItem));
}
// Attempt to first get the user settings
getSettings_(settingType)
.done(settings => {
// Cache in the local storage and resolve
window.localStorage.setItem(
LOCAL_STORAGE_PREFIX+settingType,
JSON.stringify(settings));
settingsCache[settingType] = settings;
resolve(settings);
})
.fail(error => {
// Assuming failure is because they don't exist
// Different failures should throw an error.
if (error.status != 404) throw error;
// Attempt to fetch default settings
$.ajax({
url: mw.util.wikiScript(),
data: {
action: 'raw',
title:
DefaultSettingPage[settingType]
},
dataType: 'json'
})
.done(settings => {
// Success fetching default settings.
// Set the user's settings.
Manager.setSettings(settingType, settings);
resolve(settings);
})
.fail(error => reject(error));
});
});
};
/* Helper function to get settings according to the given name */
Manager.withSettingsByName = function(userName, callback) {
return Manager.withSettings(
mw.util.isIPAddress(userName) ?
SettingTypes.ANONYMOUS :
SettingTypes.NORMAL,
callback);
};
/* Helper function to pass a callback to be called with the settings */
Manager.withSettings = function(settingType, callback) {
return Manager.getSettings(settingType).then(callback);
};
/* Calls the callback with all available templates.
Returns:
jQuery Promise for fetching available templates.
*/
Manager.withAvailableTemplates = function(callback) {
return $
.ajax({
url: mw.util.wikiScript(),
data: {action: 'raw', title: ALL_TEMPLATES_PAGE},
dataType: 'json',
cache: true
})
.done(callback)
.fail(error => { throw error; });
};
/* Calls the callback with all available templates separated by user type
This is the struct passed to the callback:
{
"anonymous": anonymousTemplates,
"normal": normalTemplates
}
Returns:
jQuery Promise for fetching available templates.
*/
Manager.withSeparateAvailableTemplates = function(callback) {
return Manager.withAvailableTemplates(availableTemplates => {
const anonymousTemplates = {};
const normalTemplates = {};
// For every type of template
for (const [templateType, templateList] of
Object.entries(availableTemplates)) {
// Create a new list for that specific template type
const anonymousList = anonymousTemplates[templateType] = [];
const normalList = normalTemplates[templateType] = [];
// Sort the templates to the correct lists
for (const template of templateList) {
switch (template.anon) {
case true:
anonymousList.push(template);
break;
case false:
normalList.push(template);
break;
default:
anonymousList.push(template);
normalList.push(template);
break;
}
}
}
callback({
[SettingTypes.ANONYMOUS]: anonymousTemplates,
[SettingTypes.NORMAL]: normalTemplates
});
});
};
/* Creates a span with warning links according to the given templates
Upon clicking the links, they will warn the given user using the clicked
template.
Args:
userName: User to warn upon clicking the link.
templates: An array of templates.
Each template is an object with the following attributes:
name: template name to show as the link text
template: template's page name (to be included in user talk)
description: hover description
The following attributes are optional:
watch: To watch or unwatch user talk page.
noPage: Don't include the page as first argument in the template.
title: By default, the new section's title is the page name.
Override the default title naming and put this one instead.
noTitle: Don't create a new section when adding the template.
Does not work with title.
noLinkTitle: Section title should not be a link to the page.
Does not work with title.
askExtra:
Object with one of the following:
confirm: confirm dialog.
Accepting will add param to template
prompt: prompt dialog.
Submitting will adds param together with prompt value
Returns:
The created span element.
*/
Manager.createLinksElement = function(userName, templates) {
// Create the container span
const containerElement = document.createElement('span');
const fillContainer = () => {
const fillFragment = document.createDocumentFragment();
const seperatorNode = document.createTextNode(' | ');
// Add an event listener that handles inner <a> tags
containerElement.addEventListener('click', event => {
// Travels up the DOM tree
for (let target = event.target; target !== event.currentTarget;
target = target.parentNode) {
// As soon as it finds an object with a warning template,
// activates the warning.
if (target.warningTemplate) {
event.stopPropagation();
event.preventDefault();
return this.activateWarning(userName, target.warningTemplate);
}
}
});
// Starts the container
fillFragment.appendChild(document.createTextNode('('));
/* Appends a template link together with it's event handler */
function appendTemplateElement(template) {
let element = document.createElement('a');
element.href = '#';
element.insertAdjacentText('beforeend', template.name);
element.warningTemplate = template;
fillFragment.appendChild(element);
}
// Add elements to containerElement.
// Does not add the seperator before first element
appendTemplateElement(templates[0]);
for (let i = 1; i < templates.length; i++) {
fillFragment.appendChild(seperatorNode.cloneNode());
appendTemplateElement(templates[i]);
}
// Ends the container
fillFragment.appendChild(document.createTextNode(')'));
containerElement.appendChild(fillFragment);
};
Manager.withSettings(SettingTypes.GENERAL, generalSettings => {
// According to the orientation, we can tell
// between desktop and mobile
// Shouldn't hide
if (!generalSettings.hideOptions[
window.orientation === undefined ? 'desktop' : 'mobile']){
return fillContainer();
}
// Should hide
const showLinksButton = document.createElement('button');
showLinksButton.insertAdjacentText('beforeend', 'הצג תבניות');
// Upon click, wait 400ms (to avoid misclick) and then show links.
showLinksButton.addEventListener(
'click',
() => window.setTimeout(
() => {
containerElement.removeChild(showLinksButton);
fillContainer();
},
400)
);
containerElement.appendChild(showLinksButton);
});
return containerElement;
};
/* Activates the given template on the given user
See createLinksElement for template object attributes
*/
Manager.activateWarning = function(userName, template) {
let templateString = template.template; // Full template string
const pageName = mw.config.get('wgPageName').replace(/_/g, ' ');
// Make sure to only link and not include categories & files.
const linkPageName =
([6, 14].includes(
mw.config.get('wgNamespaceNumber')) ? ':' : '') + pageName;
// If we should include a link to the page as first template argument.
if (!template.noPage) templateString += '|' + linkPageName;
// Needs extra information?
if (template.askExtra) {
// Need a prompt
if (template.askExtra.prompt) {
const userInput = prompt(template.askExtra.prompt);
// Add parameter if prompt was accepted.
if (userInput !== null) {
templateString += (template.askExtra.param + userInput.trim());
}
// Need a confirmation
} else if (
template.askExtra.confirm && confirm(template.askExtra.confirm)) {
// Add parameter if confirmed.
templateString += template.askExtra.param;
}
}
let fullText = '{{' + templateString + '}} ~~' + '~~\n';
// If template has a specific title
if (template.title) fullText = '\n==' + template.title + '==\n' + fullText;
// If template has a default title
else if (!template.noTitle) {
fullText =
'\n==' +
(template.noLinkTitle ? pageName : '[[' + linkPageName + ']]') +
'==\n' + fullText;
}
// Add the resulting string to the user talk page.
const page = new this.pageHandler('שיחת משתמש:' + userName);
if (template.prepend)
page.prepend(fullText, template.template);
else
page.append(fullText, template.template);
if (template.watch !== undefined) page.watch = template.watch;
page.submit()
.then(() => mw.notify(
' תבנית "' + template.template +
'" נרשמה בשיחת משתמש:' + userName))
.catch(error => alert('Error saving: ' + String(error)));
};
/* Class to simplify editing pages */
Manager.pageHandler = class pageHandler{
constructor(pageTitle) {
this.action = 'edit';
this.title = pageTitle;
this.summary = [];
this.format = 'json';
this.token = mw.user.tokens.get('csrfToken');
}
/* Append content to page */
append(content, summary) {
this.appendtext = (this.appendtext ? this.appendtext : '') + content;
if (summary) this.summary.push(summary);
return this;
}
/* Prepend content to page */
prepend(content, summary) {
this.prependtext = (this.prependtext ? this.prependtext : '') + content;
if (summary) this.summary.push(summary);
return this;
}
/* Get the watch flag for the page */
get watch() {
if (this.watchlist === undefined) return undefined;
return this.watchlist === 'watch';
}
/* Set the watch flag for the page */
set watch(value) { this.watchlist = value ? 'watch' : 'unwatch'; }
/* Submit the edits
Note: Object is unusable afterwards.
Returns:
Promise object with the data or error received.
*/
submit() {
this.summary = this.summary.join(', ');
return new Promise((resolve, reject) => {
$.post(mw.util.wikiScript('api'), this, data => {
if (data.error) return reject(data.error);
if (data.edit && data.edit.result == 'Success') return resolve(data);
throw data;
}).fail(error => { throw error; });
});
}
};
Manager.SettingsDialog = {
DIALOG_BOX_TEMPLATE: [
'<div id="setting64" title="הגדרות סקריפט 64">',
'<div id="setting64Tabs">',
'<ul>',
'<li><a href="#setting64AnonTab">אנונימים</a></li>',
'<li><a href="#setting64NormalTab">משתמשים</a></li>',
'</ul>',
'<div id="setting64AnonTab"></div>',
'<div id="setting64NormalTab"></div>',
'</div>',
'<div id="setting64General">',
'הסתרת תבניות:',
'<ul id="setting64HideOptions">',
'<li>',
'<input type=checkbox id="setting64HideDesktop" />',
'<label for="setting64HideDesktop">במחשב נייח</label>',
'</li>',
'<li>',
'<input type=checkbox id="setting64HideMobile" />',
'<label for="setting64HideMobile">בנייד</label>',
'</li>',
'</ul>',
'<div id="setting64AdvancedSettings" style="display: none">',
'<button id="setting64AddManual">הוספת תבנית בצורה ידנית',
'</button></br>',
'<button id="setting64PurgeCache">נקה מטמון</button></br>',
'</div>',
'<input type=checkbox id="setting64ShowAdvanced" />',
'<label for="setting64ShowAdvanced">הצג הגדרות מתקדמות</label>',
'</div>',
'</div>'].join(''),
/* Open the dialog
Args:
manager: script64Manager (Needed for helper functions)
*/
openDialog: function() {
mw.loader.using(['jquery.ui'])
.done(() => {
if (this.$dialogBox) {
this.$dialogBox.dialog('open');
return;
}
this.createDialog().dialog('open');
});
},
/* Pops open a new template creation dialog */
newTemplateDialog: function() {
const $dialog = $([
'<div id="setting64NewTemplate">',
'<form>',
'<span>כל השדות חובה.</span></br>',
'<label>שם: <input type="text" id="setting64NameField">',
'</label><br>',
'<label>תבנית: <input type"text" id="setting64TemplateField">',
'</label><br>',
'הוסף לתבניות <select id="setting64LocationField"></select>.<br>',
'הוסף עבור:</br>',
'<input type="radio" name="setting64CategoryRadio" ',
'value="anon">אנונימים</br>',
'<input type="radio" name="setting64CategoryRadio" ',
'value="normal">משתמשים</br>',
'<input type="radio" name="setting64CategoryRadio" ',
'value="both" checked="checked">שניהם<br>',
'<input type="submit" value="הוספה" tabindex="-1" ',
'style="position:absolute; top:-1000px">',
'</form>',
'</div>'
].join(''));
const selectMenu = $dialog[0].querySelector('#setting64LocationField');
// Add all options for the select menu.
[ // [title, warningType]
['אזהרה', 'rollbackWarnings'],
['מחיקה', 'deletionWarnings'],
['חסימה', 'blockWarnings']
].forEach(([title, warningType]) => {
const optionElement = document.createElement('option');
optionElement.warningType = warningType;
optionElement.text = title;
selectMenu.appendChild(optionElement);
});
const form = $dialog[0].firstChild;
// Save the template
const saveTemplate = () => {
const template = {};
// Extract name and template, check they are not empty.
template.name = document.getElementById('setting64NameField').value;
template.template =
document.getElementById('setting64TemplateField').value;
if (template.name.length === 0 || template.template.length === 0){
form.firstChild.classList.add('ui-state-error');
return;
}
template.description = 'הוספת תבנית ' + template.name;
// Add to anon normal or both
const selectedRadio =
form.querySelector('input[name="setting64CategoryRadio"]:checked')
.value;
const warningType =
selectMenu.options[selectMenu.selectedIndex].warningType;
// Append the templates to the appropriate sortables
switch (selectedRadio){
case 'anon':
this.sortables[SettingTypes.ANONYMOUS][warningType]
.appendTemplate(template);
break;
case 'normal':
this.sortables[SettingTypes.NORMAL][warningType]
.appendTemplate(template);
break;
case 'both':
this.sortables[SettingTypes.ANONYMOUS][warningType]
.appendTemplate(template);
this.sortables[SettingTypes.NORMAL][warningType]
.appendTemplate(template);
break;
default:
throw 'Error: Unknown radio selected';
}
$dialog.dialog('close');
$dialog.empty();
};
// Support the submit button (enter key)
form.addEventListener('submit', event => {
event.preventDefault();
saveTemplate();
});
// Create the dialog and show it.
$dialog.dialog({
autoOpen: true,
height: 300,
width: 250,
modal: true,
buttons: {
'הוסף': saveTemplate,
'סגור': () => {
$dialog.dialog('close');
$dialog.empty();
}
}
});
},
/* Represents a single warning sortable
The warning sortable is a jquery ui sortable
Args:
templates: list of templates for the sortable
available: list of available template options
Attributes:
mainNode: Main node (in this case a document fragment) that holds
the sortable. Append this to wherever you want the sortable to exist.
*/
WarningSortable: class WarningSortable{
constructor(templates, available){
// Initialize the sortable
const mainNode = this.mainNode = document.createDocumentFragment();
const sortable = this.sortable_ = document.createElement('ul');
const selectElement =
this.selectElement_ = document.createElement('select');
const templateNames = this.templateNames_ = new Set();
const appendButton = document.createElement('span');
sortable.className = 'script-64-sortable';
appendButton.className = 'ui-icon ui-icon-plus';
// Binds close button to "li" removal from DOM.
sortable.addEventListener('click', event => {
// Travels up the DOM tree
for (let target = event.target; target !== event.currentTarget;
target = target.parentNode) {
// As soon as we find the close button
// remove it's parent from the sortable
if (target.className === 'ui-icon ui-icon-close') {
const targetParent = target.parentNode;
event.stopPropagation();
event.preventDefault();
sortable.removeChild(targetParent);
// Remove it from currently used names
// and append it to select menu.
templateNames.delete(targetParent.warningTemplate.name);
this.appendAvailableTemplate(targetParent.warningTemplate);
return;
}
}
});
// Append all templates to the sortable.
templates.forEach(this.appendTemplate, this);
// Append all available warnings as options in
// the select element.
available.forEach(this.appendAvailableTemplate, this);
const $sortable = $(sortable);
// Bind the append button to append the selected item.
appendButton.addEventListener('click', () => {
const optionElement =
selectElement.options[selectElement.selectedIndex];
templateNames.delete(optionElement.warningTemplate.name);
this.appendTemplate(optionElement.warningTemplate);
// Remove the option from the select menu.
selectElement.removeChild(optionElement);
$sortable.sortable();
});
// Create the sortable
$sortable.sortable();
$sortable.disableSelection();
// Create the main fragment.
mainNode.appendChild(sortable);
mainNode.appendChild(selectElement);
mainNode.appendChild(appendButton);
}
// Appends an available template to the select menu.
appendAvailableTemplate(template){
// Avoid duplicate templates.
if (this.templateNames_.has(template.name)) return;
const optionElement = document.createElement('option');
optionElement.text = template.name;
optionElement.warningTemplate = template;
this.selectElement_.appendChild(optionElement);
this.templateNames_.add(template.name);
}
// Appends a single template as a list item.
appendTemplate(template){
// Avoid duplicate templates.
if (this.templateNames_.has(template.name)) return;
const listItem = document.createElement('li');
const closeButton = document.createElement('span');
const arrowIcon = document.createElement('span');
closeButton.className = 'ui-icon ui-icon-close';
arrowIcon.className = 'ui-icon ui-icon-arrowthick-2-n-s';
// Build the list item
listItem.className = 'ui-state-default';
listItem.warningTemplate = template;
listItem.appendChild(closeButton);
listItem.appendChild(arrowIcon);
listItem.insertAdjacentText('beforeend', template.name);
// Append it.
this.sortable_.appendChild(listItem);
this.templateNames_.add(template.name);
}
get templates() {
return [...this.sortable_.children].map(li => li.warningTemplate);
}
},
/* Creates the dialog box
Returns:
The dialog box as a jQuery element
*/
createDialog: function() {
const $dialogBox = this.$dialogBox = $(this.DIALOG_BOX_TEMPLATE);
this.sortables = {};
/* Creates the sortables
Args:
currentSettings: a settings JSON
availableWarnings: available warnings JSON
Returns:
[DocumentFragment containing warning sortables,
Object mapping warningType: sortableInstance]
*/
const createWarningSortables = (currentSettings, availableWarnings) => {
const fragment = document.createDocumentFragment();
const warningTemplate = document.createElement('div');
const sortables = {};
warningTemplate.className = 'script-64-warnings';
warningTemplate.appendChild(document.createElement('h2'));
// Create all three sortables.
[ // [title, warningType]
['תבניות לאזהרה', 'rollbackWarnings'],
['תבניות למחיקת דפים', 'deletionWarnings'],
['תבניות לחסימה', 'blockWarnings']
].forEach(([title, warningType]) => {
const sortable = new this.WarningSortable(
currentSettings[warningType], availableWarnings[warningType]);
sortables[warningType] = sortable;
const warningElement = warningTemplate.cloneNode(true);
warningElement.firstChild.textContent = title;
warningElement.dataset.settingName = warningType;
warningElement.appendChild(sortable.mainNode);
fragment.appendChild(warningElement);
});
return [fragment, sortables];
};
const anonymousTab = $dialogBox.find('#setting64AnonTab');
const normalTab = $dialogBox.find('#setting64NormalTab');
/* Build the anonymous and user tabs
Args:
advanced: Show advanced settings
*/
const showTabs = (advanced=false) => {
/* Get a combined version of the available templates
Sets all templates to be available in all cases.
Args:
availableTemplates: the availableTemplates JSON
Returns:
an availableTemplates JSON with all templates available in
all cases.
*/
function combineTemplates(availableTemplates){
const allTemplates = [].concat(...Object.values(availableTemplates));
const combinedTemplates = {};
// Clone and set all templates.
for (const key of Object.keys(availableTemplates)){
combinedTemplates[key] = [...allTemplates];
}
return combinedTemplates;
}
Manager.withSeparateAvailableTemplates(separateTemplates => {
Manager.withSettings(SettingTypes.ANONYMOUS, settings => {
let fragment, anonSortables;
if (advanced){
// Create the advanced warning sortable
[fragment, anonSortables] =
createWarningSortables(
settings,
combineTemplates(
separateTemplates[SettingTypes.ANONYMOUS]));
} else {
// Create the normal warning sortable
[fragment, anonSortables] =
createWarningSortables(
settings,
separateTemplates[SettingTypes.ANONYMOUS]);
}
anonymousTab.append(fragment);
this.sortables[SettingTypes.ANONYMOUS] = anonSortables;
});
Manager.withSettings(SettingTypes.NORMAL, settings => {
let fragment, normalSortables;
if (advanced){
// Create the advanced warning sortable
[fragment, normalSortables] =
createWarningSortables(
settings,
combineTemplates(
separateTemplates[SettingTypes.NORMAL]));
} else {
// Create the normal warning sortable
[fragment, normalSortables] =
createWarningSortables(
settings,
separateTemplates[SettingTypes.NORMAL]);
}
normalTab.append(fragment);
this.sortables[SettingTypes.NORMAL] = normalSortables;
});
});
};
showTabs();
// If advanced checkbox is checked, reload with advanced settings.
$dialogBox.find('#setting64ShowAdvanced').change(event => {
const advnaced = event.currentTarget.checked;
anonymousTab.empty();
normalTab.empty();
showTabs(advnaced);
if (advnaced)
$dialogBox.find('#setting64AdvancedSettings')
.css("display", "block");
else
$dialogBox.find('#setting64AdvancedSettings')
.css("display", "none");
});
// General settings
Manager.withSettings(SettingTypes.GENERAL, generalSettings => {
$dialogBox.find('#setting64HideDesktop')
.prop('checked', generalSettings.hideOptions.desktop);
$dialogBox.find('#setting64HideMobile')
.prop('checked', generalSettings.hideOptions.mobile);
});
$dialogBox.find("#setting64AddManual")
.click(this.newTemplateDialog.bind(this));
$dialogBox.find("#setting64PurgeCache").click(() => {
Object.values(SettingTypes).forEach(purgeCache_, this.Manager);
console.log("Purged cache.");
});
$dialogBox.dialog({
autoOpen: false,
height: 400,
width: 350,
modal: true,
buttons: {
'שמירה': () => {
// Save and notify in case of success
this.saveBox()
.done(() => mw.notify('הגדרות סקריפט 64 נשמרו בהצלחה.'));
$dialogBox.dialog('close');
},
'סגירה': () => $dialogBox.dialog('close')
}
});
// Activate the tabs
$dialogBox.find('#setting64Tabs').tabs();
return $dialogBox;
},
/* Save the data in the dialog box.
Returns:
A jQuery promise. .done() will be called when settings were saved
successfully.
*/
saveBox: function() {
function extractSettings(sortables) {
const settings = {};
for (const [warningType, sortable] of Object.entries(sortables)){
settings[warningType] = sortable.templates;
}
return settings;
}
const generalSettings = {
hideOptions: {
desktop: document.getElementById('setting64HideDesktop').checked,
mobile: document.getElementById('setting64HideMobile').checked,
}
};
return $.when(
// Set anonymous settings
Manager.setSettings(
SettingTypes.ANONYMOUS,
extractSettings(this.sortables[SettingTypes.ANONYMOUS])),
// Set... Well... Guess.
Manager.setSettings(
SettingTypes.NORMAL,
extractSettings(this.sortables[SettingTypes.NORMAL])),
Manager.setSettings(SettingTypes.GENERAL, generalSettings)
);
},
};
/* Start point for the script. */
Manager.loadManager = function() {
this.mainElement = document.getElementById('mw-content-text');
mw.util.addPortletLink('p-tb', '#', 'הגדרות סקריפט 64').onclick =
this.SettingsDialog.openDialog.bind(this.SettingsDialog);
// Execute handlers until one of them returns a true value
for (const handler of this.handlers) {
if (handler(this)) break;
}
};
return Manager;
}();
// Check if we blocked a user
script64Manager.handlers.push(function(manager) {
'use strict';
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Block') return;
/* Args: Element to insert, Node to insert the element after */
function insertSibling(insert, node) {
return node.parentNode.insertBefore(insert, node.nextSibling);
}
// The name of the link is the userName.
const userlinkNode = manager.mainElement.getElementsByTagName('a')[0];
const userName = userlinkNode.firstChild.nodeValue;
const spaceNode = document.createTextNode(' ');
// Add a spacer
insertSibling(spaceNode, userlinkNode.parentNode);
// Create the main warning element and insert it.
manager.withSettingsByName(userName, settings => {
insertSibling(
manager.createLinksElement(userName, settings.blockWarnings),
spaceNode);
});
return true;
});
// Check if it's a diff
script64Manager.handlers.push(function(manager) {
'use strict';
// Diffs have this element
const diffElement = document.getElementById('mw-diff-ntitle2');
if (diffElement === null) return;
// The bi-directional element holds a text node with the userName
const userName =
diffElement.getElementsByTagName('bdi')[0].firstChild.nodeValue;
// Create a new paragraph to hold all links.
const paragraph = document.createElement('p');
// Create the main warning element, insert into the paragraph, and append it.
manager.withSettingsByName(userName, settings => {
paragraph.appendChild(
manager.createLinksElement(userName, settings.rollbackWarnings));
diffElement.appendChild(paragraph);
});
return true;
});
// Check for a rollback
script64Manager.handlers.push(function(manager) {
'use strict';
if (mw.config.get('wgAction') !== 'rollback') return;
// userName is the text of the first bi-directional element.
const userElement = manager.mainElement.getElementsByTagName('bdi')[0];
if (!userElement) return; // Rollback failed
const userToolLinksNode = userElement.parentNode.nextElementSibling;
const userName = userElement.firstChild.nodeValue;
// Assuming we're in the right place
console.assert(
userToolLinksNode.className == 'mw-usertoollinks',
userToolLinksNode.className);
/* Args: Element to insert, Node to insert the element after */
function insertSibling(insert, node) {
return node.parentNode.insertBefore(insert, node.nextSibling);
}
// Create some warning indicators
const fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode(' ('));
// Full status span (Block and warn)
const statusSpan = document.createElement('span');
statusSpan.id = 'script-64-status-span';
const warnedSpan = document.createElement('span');
warnedSpan.textContent = 'הוזהר';
statusSpan.appendChild(warnedSpan);
statusSpan.insertAdjacentText('beforeend', ' ');
const blockSpan = document.createElement('span');
blockSpan.textContent = 'חסום';
statusSpan.appendChild(blockSpan);
fragment.appendChild(statusSpan);
fragment.appendChild(document.createTextNode(') '));
// Append the links element to the fragment and insert it.
manager.withSettingsByName(userName, settings => {
fragment.appendChild(
manager.createLinksElement(userName, settings.rollbackWarnings));
insertSibling(fragment, userToolLinksNode);
});
const api = new mw.Api();
// If a block was found, turn on the indicators.
function indicateBlocks(data){
if (data && data.query && data.query.blocks && data.query.blocks &&
data.query.blocks.length) {
// Light the indicators! We have a freak on the loose!
blockSpan.className = 'script-64-activated-indicator';
blockSpan.title =
'נחסם על ידי ' + decodeURIComponent(data.query.blocks[0].by);
}
}
// Get block status and light indicators accordingly
if (mw.util.isIPAddress(userName)) {
api.get({list: 'blocks', bkip: userName}).done(indicateBlocks);
} else {
api.get({list: 'blocks', bkusers: userName}).done(indicateBlocks);
}
// Modify warning indicator by checking the user's talk pages'
// recent revision summaries for warning templates.
api.get({
action: 'query',
titles: 'שיחת משתמש:' + userName,
prop: 'revisions',
indexpageids: true,
rvprop: ['user', 'timestamp', 'comment'],
rvlimit: 5,
format: 'json',
})
.done(data => {
const pageid = data.query.pageids[0];
if (pageid === '-1') return; // User talk page not found
manager.withAvailableTemplates(templates => {
const revisionList = data.query.pages[pageid].revisions;
const timeThreshold = mw.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
// Concat all available templates into one big list
const templatesList = [].concat(...Object.values(templates));
// Extract the template's name from all templates and turn into a set.
const templateNames =
new Set(templatesList.map(template => template.template));
// Iterate over all revisions, matching revision summary
// with a template name. If a match is found, and the revision isn't
// older than the set threshold, light the indicators.
for (const revision of revisionList) {
const revisionDate = new Date(revision.timestamp);
// Revision passed the time threshold
// Since they are sorted by date, all later revisions also
// passed the threshold. Stop the iteration.
if (revisionDate < timeThreshold) return;
// Match summary with template name.
if (!templateNames.has(revision.comment)) continue;
// Found a match. Light the indicators! Ahhhh!!! Panic.
warnedSpan.title =
`הוזהר על ידי ${revision.user} ` +
`בתאריך ${revisionDate.toLocaleString()}.`;
warnedSpan.className = 'script-64-activated-indicator';
return;
}
});
});
return true;
});
// Check for page deletion
script64Manager.handlers.push(function(manager) {
'use strict';
// If it's not a deletion
if (mw.config.get('wgAction') !== 'delete' ||
// Or user is not a sysop
!mw.config.get('wgUserGroups').includes('sysop') ||
// Or deletion was not successful (entered confirmation page)
document.getElementById('deleteconfirm'))
return;
// Query the API for last 20 deleted revisions
$.getJSON(
mw.util.wikiScript('api'),
{
action: 'query',
list: 'deletedrevs',
drlimit: 20,
titles: mw.config.get('wgPageName'),
drprop: 'user',
format: 'json'
},
function(data) {
const users = new Set();
if (data && data.query && data.query.deletedrevs)
// Add all users who made a change in the last 20 revisions.
data.query.deletedrevs.forEach(
(deletedrev) => deletedrev.revisions.forEach(
(revision) => users.add(revision.user)));
else
throw 'No deleted revisions..?';
// Create a container for all users who modified the page.
const container = document.createElement('p');
container.insertAdjacentText(
'beforeend',
`את הדף ${mw.config.get('wgPageName')} ערכו העורכים הבאים:`);
/* Creates basic links for a user.
Link to the User page, User Talk, Contributions and block.
Returns:
a DocumentFragment.
*/
function createUserLinks(userName) {
function createLink(url, text) {
const element = document.createElement('a');
element.setAttribute('href', url);
element.textContent = text;
return element;
}
const fragment = document.createDocumentFragment();
fragment.appendChild(
createLink(mw.util.getUrl('User:' + userName), userName));
fragment.appendChild(document.createTextNode(' ('));
fragment.appendChild(
createLink(mw.util.getUrl('User Talk:' + userName), 'שיחה'));
fragment.appendChild(document.createTextNode(' | '));
fragment.appendChild(createLink(
mw.util.getUrl('Special:Contributions/' + userName), 'תרומות'));
fragment.appendChild(document.createTextNode(' | '));
fragment.appendChild(
createLink(mw.util.getUrl('Special:Block/' + userName), 'חסימה'));
fragment.appendChild(document.createTextNode(' ) '));
return fragment;
}
// For each user, adds his links and warnings.
users.forEach(
// No need to worry, withSettingsByName is cached.
user => manager.withSettingsByName(user, settings => {
const userContainer = document.createElement('p');
userContainer.appendChild(createUserLinks(user));
userContainer.appendChild(
manager.createLinksElement(
user, settings.deletionWarnings));
container.appendChild(userContainer);
}));
// Append the entire container.
manager.mainElement.appendChild(container);
});
return true;
});
/* Function to load all necessary polyfills.
Returns:
a combined jQuery Promise for when all polyfills finish loading.
*/
script64Manager.loadPolyfills = function(){
'use strict';
/* jshint unused:false */
// Loads a single script with caching turned on.
function loadScript(url){
return $.ajax({
url: url,
dataType: 'script',
cache: true,
});
}
/***** Starting Polyfills *****/
/* jshint ignore:start */
const reduce =
Function.bind.call(Function.call, Array.prototype.reduce);
const isEnumerable =
Function.bind.call(Function.call, Object.prototype.propertyIsEnumerable);
const concat =
Function.bind.call(Function.call, Array.prototype.concat);
const keys = Reflect.ownKeys;
/** Object.values polyfill **/
if (!Object.values) {
Object.values = function values(O) {
return reduce(
keys(O),
(v, k) => concat(
v, typeof k === 'string' && isEnumerable(O, k) ? [O[k]] : []),
[]);
};
}
/** Object.entries polyfill **/
if (!Object.entries) {
Object.entries = function entries(O) {
return reduce(
keys(O),
(e, k) => concat(
e,
typeof k === 'string' &&
isEnumerable(O, k) ? [[k, O[k]]] : []),
[]);
};
}
/** Array.includes polyfill **/
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, 'includes', {
value: function(searchElement, fromIndex) {
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
var o = Object(this);
var len = o.length >>> 0;
if (len === 0) {
return false;
}
var n = fromIndex | 0;
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
while (k < len) {
if (o[k] === searchElement) {
return true;
}
k++;
}
return false;
}
});
}
/* jshint ignore:end */
/***** End of polyfills *****/
return $.when();
};
/*** Load script ***/
script64Manager.loadPolyfills().done(() => {
'use strict';
// Wait for page to load.
if (!['loaded', 'complete','interactive'].includes(document.readyState))
document.addEventListener(
'DOMContentLoaded',
script64Manager.loadManager.bind(script64Manager));
// Activate if page was already loaded.
else script64Manager.loadManager();
});