User:Saroj/CatSync.js: Difference between revisions
Appearance
Content deleted Content added
// Edit via Wikiplus |
Add mobile (Minerva) support via p-tb |
||
| Line 360: | Line 360: | ||
}; |
}; |
||
// ---------------- Dialog 2: preview & |
// ---------------- Dialog 2: preview & add ---------------- |
||
function PreviewDialog(config) { PreviewDialog.super.call(this, config); } |
function PreviewDialog(config) { PreviewDialog.super.call(this, config); } |
||
| Line 367: | Line 367: | ||
PreviewDialog.static.title = 'Categories to add'; |
PreviewDialog.static.title = 'Categories to add'; |
||
PreviewDialog.static.actions = [ |
PreviewDialog.static.actions = [ |
||
{ action: ' |
{ action: 'add', label: 'Add', flags: ['primary', 'progressive'] }, |
||
{ label: 'Cancel', flags: 'safe' } |
{ label: 'Cancel', flags: 'safe' } |
||
]; |
]; |
||
| Line 406: | Line 406: | ||
PreviewDialog.prototype.getActionProcess = function (action) { |
PreviewDialog.prototype.getActionProcess = function (action) { |
||
var dialog = this; |
var dialog = this; |
||
if (action === ' |
if (action === 'add') { |
||
return new OO.ui.Process(function () { |
return new OO.ui.Process(function () { |
||
var selected = dialog.checkboxes.getValue(); |
var selected = dialog.checkboxes.getValue(); |
||
| Line 416: | Line 416: | ||
dialog.popPending(); |
dialog.popPending(); |
||
dialog.close(); |
dialog.close(); |
||
mw.notify(selected.length + (selected.length === 1 ? ' category' : ' categories') + ' |
mw.notify(selected.length + (selected.length === 1 ? ' category' : ' categories') + ' added. Reloading…', { type: 'success' }); |
||
setTimeout(function () { window.location.reload(); }, 1200); |
setTimeout(function () { window.location.reload(); }, 1200); |
||
}, function () { |
}, function () { |
||
| Line 454: | Line 454: | ||
} |
} |
||
// use Tools portlet on Minerva (mobile) |
|||
| ⚫ | |||
var skin = mw.config.get('skin'); |
|||
var portlet = skin === 'minerva' ? 'p-tb' : 'p-cactions'; |
|||
| ⚫ | |||
$('#ca-synccategories a').on('click', function (e) { |
$('#ca-synccategories a').on('click', function (e) { |
||
e.preventDefault(); |
e.preventDefault(); |
||
Latest revision as of 13:47, 3 August 2026
// <nowiki>
(function() {
'use strict';
mw.loader.using([
'mediawiki.util',
'mediawiki.api',
'oojs-ui-core',
'oojs-ui-widgets',
'oojs-ui-windows'
]).then(function () {
if (mw.config.get('wgNamespaceNumber') !== 0 || mw.config.get('wgAction') !== 'view') {
return;
}
var currentTitle = mw.config.get('wgPageName').replace(/_/g, ' ');
var localDbName = mw.config.get('wgDBname');
var localCategoryNs = mw.config.get('wgFormattedNamespaces')['14'];
// Note: Wikipedia's dbSuffix is 'wiki', not 'wikipedia'.
var PROJECTS = [
{ id: 'wikipedia', label: 'Wikipedia', domain: 'wikipedia.org', dbSuffix: 'wiki' },
{ id: 'wikiquote', label: 'Wikiquote', domain: 'wikiquote.org', dbSuffix: 'wikiquote' }
];
function getProject(id) {
var found = null;
$.each(PROJECTS, function (i, p) { if (p.id === id) { found = p; return false; } });
return found || PROJECTS[0];
}
// Runs only on Wikipedia or Wikiquote; anywhere else, exit before any setup.
function detectLocalProject() {
var byLongestSuffix = PROJECTS.slice().sort(function (a, b) { return b.dbSuffix.length - a.dbSuffix.length; });
var match = null;
$.each(byLongestSuffix, function (i, p) {
if (localDbName.indexOf(p.dbSuffix, localDbName.length - p.dbSuffix.length) !== -1) {
match = p;
return false;
}
});
return match;
}
var localProject = detectLocalProject();
if (!localProject) {
return;
}
// ---------------- data layer ----------------
function crossApi(domain, params) {
params.format = 'json';
params.origin = '*';
return $.getJSON('https://' + domain + '/w/api.php', params);
}
// Fallback until the language list resolves.
var FALLBACK_LANGS = [
{ data: 'en', label: 'English (en)' },
{ data: 'hi', label: 'Hindi (hi)' },
{ data: 'ne', label: 'Nepali (ne)' }
];
// Same-origin call to this wiki's own interface languages list.
function fetchWikipediaLanguages() {
var d = $.Deferred();
new mw.Api().get({
action: 'query',
meta: 'siteinfo',
siprop: 'languages',
formatversion: 2
}).done(function (data) {
var raw = (data.query && data.query.languages) || [];
if (raw.length === 0) {
d.resolve(FALLBACK_LANGS);
return;
}
var langs = raw.map(function (l) {
return { data: l.code, label: l.name + ' (' + l.code + ')' };
});
var enIndex = langs.findIndex(function (l) { return l.data === 'en'; });
if (enIndex > -1) {
langs.unshift(langs.splice(enIndex, 1)[0]);
}
d.resolve(langs);
}).fail(function () { d.resolve(FALLBACK_LANGS); });
return d.promise();
}
var wikipediaLangs = FALLBACK_LANGS;
var languagesPromise = fetchWikipediaLanguages().done(function (langs) { wikipediaLangs = langs; });
function chunk(arr, size) {
var out = [];
for (var i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
return out;
}
function getLocalWikidataItem() {
var d = $.Deferred();
var existing = mw.config.get('wgWikibaseItemId');
if (existing) {
return d.resolve(existing).promise();
}
new mw.Api().get({
action: 'query',
prop: 'pageprops',
ppprop: 'wikibase_item',
titles: currentTitle,
formatversion: 2
}).done(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var qid = page && page.pageprops && page.pageprops.wikibase_item;
if (qid) d.resolve(qid); else d.reject('no-item');
}).fail(function () { d.reject('no-item'); });
return d.promise();
}
function getSourceArticleTitle(qid, sourceLang, project) {
var d = $.Deferred();
var siteId = sourceLang + project.dbSuffix;
crossApi('www.wikidata.org', {
action: 'wbgetentities',
ids: qid,
props: 'sitelinks',
sitefilter: siteId
}).done(function (data) {
var entity = data.entities && data.entities[qid];
var link = entity && entity.sitelinks && entity.sitelinks[siteId];
if (link && link.title) d.resolve(link.title); else d.reject('no-source-article');
}).fail(function () { d.reject('no-source-article'); });
return d.promise();
}
function getSourceCategories(sourceLang, project, sourceTitle) {
var d = $.Deferred();
crossApi(sourceLang + '.' + project.domain, {
action: 'query',
titles: sourceTitle,
prop: 'categories',
cllimit: 'max',
clshow: '!hidden',
formatversion: 2
}).done(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var cats = ((page && page.categories) || []).map(function (c) {
return c.title.replace(/^[^:]+:/, '').trim();
});
d.resolve(cats);
}).fail(function () { d.resolve([]); });
return d.promise();
}
// sourceLang category name -> QID
function getQidsForCategories(sourceLang, project, categories) {
var d = $.Deferred();
var siteId = sourceLang + project.dbSuffix;
var titles = categories.map(function (c) { return 'Category:' + c; });
var map = {};
var calls = chunk(titles, 50).map(function (batch) {
return crossApi('www.wikidata.org', {
action: 'wbgetentities',
sites: siteId,
titles: batch.join('|'),
props: 'sitelinks',
sitefilter: siteId
}).done(function (data) {
$.each(data.entities || {}, function (qid, entity) {
if (qid === '-1') return;
var link = entity.sitelinks && entity.sitelinks[siteId];
var title = link && link.title && link.title.replace(/^[^:]+:/, '').trim();
if (title) map[title] = qid;
});
});
});
$.when.apply($, calls).always(function () { d.resolve(map); });
return d.promise();
}
// QID -> local wiki title (namespace stripped)
function getLocalTitlesForQids(qidMap) {
var d = $.Deferred();
var qids = Object.keys(qidMap)
.map(function (k) { return qidMap[k]; })
.filter(function (v, i, a) { return a.indexOf(v) === i; });
var qidToLocal = {};
var calls = chunk(qids, 50).map(function (batch) {
return crossApi('www.wikidata.org', {
action: 'wbgetentities',
ids: batch.join('|'),
props: 'sitelinks',
sitefilter: localDbName
}).done(function (data) {
$.each(data.entities || {}, function (qid, entity) {
var link = entity.sitelinks && entity.sitelinks[localDbName];
if (link && link.title) {
qidToLocal[qid] = link.title.replace(/^[^:]+:/, '').trim();
}
});
});
});
$.when.apply($, calls).always(function () { d.resolve(qidToLocal); });
return d.promise();
}
function getExistingLocalCategories() {
var d = $.Deferred();
new mw.Api().get({
action: 'query',
titles: currentTitle,
prop: 'categories',
cllimit: 'max',
formatversion: 2
}).done(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var cats = ((page && page.categories) || []).map(function (c) {
return c.title.replace(/^[^:]+:/, '').trim().toLowerCase();
});
d.resolve(cats);
}).fail(function () { d.resolve([]); });
return d.promise();
}
// Full pipeline -> { toAdd, alreadyCount, missingCount, total }
function runSync(sourceLang, project) {
var d = $.Deferred();
getLocalWikidataItem().then(function (qid) {
return getSourceArticleTitle(qid, sourceLang, project);
}).then(function (sourceTitle) {
return getSourceCategories(sourceLang, project, sourceTitle);
}).then(function (sourceCats) {
if (sourceCats.length === 0) {
d.resolve({ toAdd: [], alreadyCount: 0, missingCount: 0, total: 0 });
return;
}
$.when(
getQidsForCategories(sourceLang, project, sourceCats),
getExistingLocalCategories()
).done(function (qidMap, existingCats) {
getLocalTitlesForQids(qidMap).done(function (qidToLocal) {
var localNames = Object.keys(qidMap)
.map(function (src) { return qidToLocal[qidMap[src]]; })
.filter(Boolean);
localNames = localNames.filter(function (v, i, a) { return a.indexOf(v) === i; });
var missingCount = sourceCats.length - localNames.length;
var toAdd = [];
var alreadyCount = 0;
localNames.forEach(function (name) {
if (existingCats.indexOf(name.toLowerCase()) !== -1) {
alreadyCount++;
} else {
toAdd.push(name);
}
});
d.resolve({ toAdd: toAdd, alreadyCount: alreadyCount, missingCount: missingCount, total: sourceCats.length });
});
}).fail(function () { d.reject('fetch-failed'); });
}, function (reason) { d.reject(reason); });
return d.promise();
}
function insertCategories(sourceLang, project, names) {
var lines = names.map(function (n) { return '[[' + localCategoryNs + ':' + n + ']]'; }).join('\n');
return new mw.Api().postWithToken('csrf', {
action: 'edit',
title: currentTitle,
appendtext: '\n' + lines,
summary: 'Synced ' + names.length + (names.length === 1 ? ' category' : ' categories') + ' from ' + sourceLang + project.dbSuffix + ' ([[:m:User:Saroj/CatSync|CatSync]])',
minor: false
});
}
// ---------------- Dialog 1: choose source wiki ----------------
function SourceDialog(config) { SourceDialog.super.call(this, config); }
OO.inheritClass(SourceDialog, OO.ui.ProcessDialog);
SourceDialog.static.name = 'categorySyncSource';
SourceDialog.static.title = 'Sync categories';
SourceDialog.static.actions = [
{ action: 'sync', label: 'Sync', flags: ['primary', 'progressive'] },
{ label: 'Cancel', flags: 'safe' }
];
SourceDialog.prototype.initialize = function () {
SourceDialog.super.prototype.initialize.call(this);
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.projectDropdown = new OO.ui.DropdownInputWidget({
options: PROJECTS.map(function (p) { return { data: p.id, label: p.label }; }),
value: getProject('wikipedia').id
});
// Selecting an option fills in its code; typing an unlisted code works too.
this.langInput = new OO.ui.ComboBoxInputWidget({
options: wikipediaLangs,
value: 'en',
placeholder: 'e.g. bn, hi, ja'
});
this.fieldset = new OO.ui.FieldsetLayout({
label: 'Copy categories from the same article on another project, matched via Wikidata.'
});
this.fieldset.addItems([
new OO.ui.FieldLayout(this.projectDropdown, { label: 'Source project', align: 'top' }),
new OO.ui.FieldLayout(this.langInput, {
label: 'Source language',
align: 'top',
help: 'Search or type a language code.',
helpInline: true
})
]);
this.panel.$element.append(this.fieldset.$element);
this.$body.append(this.panel.$element);
};
SourceDialog.prototype.getSetupProcess = function (data) {
return SourceDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
if (wikipediaLangs.length > FALLBACK_LANGS.length) {
var current = this.langInput.getValue();
this.langInput.setOptions(wikipediaLangs);
this.langInput.setValue(current || 'en');
}
}, this);
};
SourceDialog.prototype.getBodyHeight = function () { return 240; };
SourceDialog.prototype.getActionProcess = function (action) {
var dialog = this;
if (action === 'sync') {
return new OO.ui.Process(function () {
var sourceLang = dialog.langInput.getValue().trim().toLowerCase();
var project = getProject(dialog.projectDropdown.getValue());
if (!sourceLang) {
return $.Deferred().reject(new OO.ui.Error('Enter a language code.')).promise();
}
dialog.pushPending();
return runSync(sourceLang, project).then(function (result) {
dialog.popPending();
dialog.close();
openPreview(sourceLang, project, result);
}, function (reason) {
dialog.popPending();
var messages = {
'no-item': 'This page is not connected to a Wikidata item.',
'no-source-article': 'No article exists on ' + sourceLang + '.' + project.domain + ' for this item.',
'fetch-failed': 'Something went wrong while fetching categories. Try again.'
};
return $.Deferred().reject(new OO.ui.Error(messages[reason] || 'Sync failed.')).promise();
});
});
}
return SourceDialog.super.prototype.getActionProcess.call(this, action);
};
// ---------------- Dialog 2: preview & add ----------------
function PreviewDialog(config) { PreviewDialog.super.call(this, config); }
OO.inheritClass(PreviewDialog, OO.ui.ProcessDialog);
PreviewDialog.static.name = 'categorySyncPreview';
PreviewDialog.static.title = 'Categories to add';
PreviewDialog.static.actions = [
{ action: 'add', label: 'Add', flags: ['primary', 'progressive'] },
{ label: 'Cancel', flags: 'safe' }
];
PreviewDialog.prototype.initialize = function () {
PreviewDialog.super.prototype.initialize.call(this);
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.summary = new OO.ui.LabelWidget({ label: '' });
this.checkboxes = new OO.ui.CheckboxMultiselectInputWidget({ options: [] });
this.panel.$element.append(
this.summary.$element,
$('<div>').css('margin-top', '12px').append(this.checkboxes.$element)
);
this.$body.append(this.panel.$element);
};
PreviewDialog.prototype.getSetupProcess = function (data) {
return PreviewDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
this.sourceLang = data.sourceLang;
this.project = data.project;
var sourceLabel = data.sourceLang + '.' + data.project.domain.replace(/\.org$/, '');
var parts = [data.toAdd.length + ' new categories found from ' + sourceLabel];
if (data.alreadyCount) parts.push(data.alreadyCount + ' already present');
if (data.missingCount) parts.push(data.missingCount + ' with no equivalent here');
this.summary.setLabel(parts.join(' · '));
this.checkboxes.setOptions(data.toAdd.map(function (name) {
return { data: name, label: name };
}));
// Per-option 'selected' is ignored; set via setValue() instead.
this.checkboxes.setValue(data.toAdd);
}, this);
};
PreviewDialog.prototype.getBodyHeight = function () { return 320; };
PreviewDialog.prototype.getActionProcess = function (action) {
var dialog = this;
if (action === 'add') {
return new OO.ui.Process(function () {
var selected = dialog.checkboxes.getValue();
if (selected.length === 0) {
return $.Deferred().reject(new OO.ui.Error('Select at least one category.')).promise();
}
dialog.pushPending();
return insertCategories(dialog.sourceLang, dialog.project, selected).then(function () {
dialog.popPending();
dialog.close();
mw.notify(selected.length + (selected.length === 1 ? ' category' : ' categories') + ' added. Reloading…', { type: 'success' });
setTimeout(function () { window.location.reload(); }, 1200);
}, function () {
dialog.popPending();
return $.Deferred().reject(new OO.ui.Error('Could not save the page. Check your edit permissions.')).promise();
});
});
}
return PreviewDialog.super.prototype.getActionProcess.call(this, action);
};
// ---------------- wiring ----------------
var windowManager = new OO.ui.WindowManager();
$(document.body).append(windowManager.$element);
var sourceDialog = new SourceDialog();
var previewDialog = new PreviewDialog();
windowManager.addWindows([sourceDialog, previewDialog]);
function openPreview(sourceLang, project, result) {
if (result.toAdd.length === 0) {
mw.notify(
result.total === 0
? 'No categories found on the source article.'
: 'Nothing to add. All categories are already present or have no equivalent here.',
{ type: 'warn' }
);
return;
}
windowManager.openWindow(previewDialog, {
sourceLang: sourceLang,
project: project,
toAdd: result.toAdd,
alreadyCount: result.alreadyCount,
missingCount: result.missingCount
});
}
// use Tools portlet on Minerva (mobile)
var skin = mw.config.get('skin');
var portlet = skin === 'minerva' ? 'p-tb' : 'p-cactions';
mw.util.addPortletLink(portlet, '#', 'Sync categories', 'ca-synccategories', 'Copy categories from another project via Wikidata');
$('#ca-synccategories a').on('click', function (e) {
e.preventDefault();
languagesPromise.always(function () {
windowManager.openWindow(sourceDialog);
});
});
}).catch(function (err) {
console.error('[CatSync] failed to initialize:', err);
});
})();
// </nowiki>