MediaWiki:Gadget-templating.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* eslint-env browser, es6 */
/* global mw, $ */
mw.loader.using(['mediawiki.api']).then(function () {
(async function () {
const api = new mw.Api();
const userLang = mw.config.get('wgUserLanguage') || 'en';
// ---- 1) CONFIG: how we find your template items ----
// Use exactly what works in the search box:
const SEARCH_QUERY = 'haswbstatement:P1=Q196450';
// If needed, you can hardcode Q-IDs instead and skip searchTemplates().
// ---- 2) HELPERS used by the builder ----
const chunk = (arr, n) => { const out=[]; for (let i=0;i<arr.length;i+=n) out.push(arr.slice(i,i+n)); return out; };
const getBestLabel = (labels) =>
(labels?.[userLang]?.value) || (labels && Object.values(labels)[0]?.value) || undefined;
// decide if "_" means "placeholder" for string-like types
const isPlaceholderSnak = (snak) => {
if (!snak || snak.snaktype !== 'value' || !snak.datavalue) return false;
const dt = snak.datatype;
const v = snak.datavalue.value;
if (dt === 'string' || dt === 'external-id' || dt === 'url') return v === '_';
if (dt === 'monolingualtext') return v?.text === '_';
return false;
};
const cleanTemplateLabel = (raw) => (raw || '')
.replace(/^RDA\s*/i, '')
.replace(/\s*Template$/i, '')
.trim();
const slugify = (s) => (s || '')
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
// ---- 3) BUILDER: search → fetch entities → fetch property meta → transform → WbTemplates ----
async function searchTemplates(query) {
const res = await api.get({
action: 'query',
list: 'search',
srsearch: query,
// make API behave like Special:Search
srbackend: 'CirrusSearch',
srqiprofile: 'wikibase',
srlimit: 50,
formatversion: 2
});
return (res.query?.search || [])
.map(h => (h.title.match(/Q\d+$/) || [])[0])
.filter(Boolean);
}
async function fetchEntities(ids) {
const map = {};
for (const group of chunk(ids, 50)) {
const res = await api.get({
action: 'wbgetentities',
ids: group.join('|'),
props: 'labels|claims',
languages: userLang
});
Object.assign(map, res.entities || {});
}
return map;
}
async function fetchPropertyMeta(entityMap) {
const pids = new Set();
Object.values(entityMap).forEach(e => Object.keys(e.claims || {}).forEach(pid => pids.add(pid)));
const map = {};
for (const group of chunk([...pids], 50)) {
const res = await api.get({
action: 'wbgetentities',
ids: group.join('|'),
props: 'labels|datatype',
languages: userLang
});
Object.assign(map, res.entities || {});
}
// normalize
const meta = {};
for (const [pid, ent] of Object.entries(map)) {
meta[pid] = {
id: pid,
label: getBestLabel(ent.labels) || pid,
datatype: ent.datatype // e.g., 'wikibase-item', 'string', 'globe-coordinate'
};
}
return meta;
}
function transformToTemplate(entity, propMeta) {
const rawLabel = getBestLabel(entity.labels) || entity.id;
const label = cleanTemplateLabel(rawLabel);
const prefilledClaims = [];
const userDefinedClaims = [];
for (const [pid, statements] of Object.entries(entity.claims || {})) {
if (!Array.isArray(statements) || !statements.length) continue;
const prop = propMeta[pid] || { id: pid, label: pid, datatype: 'string' };
// first non-placeholder statement (if any)
const firstNonPlaceholder = statements
.map(st => st.mainsnak)
.find(ms => ms && ms.snaktype === 'value' && !isPlaceholderSnak(ms));
if (firstNonPlaceholder) {
// NOTE: to keep compatibility with your prefillStatement (which assumes wikibase-item),
// we only prefill non-placeholder values for wikibase-item. Others go to userDefined.
if (prop.datatype === 'wikibase-item') {
const v = firstNonPlaceholder.datavalue?.value; // {id: 'Qxx'}
prefilledClaims.push({
property: { id: prop.id, label: prop.label, datatype: prop.datatype },
value: { mainVal: { id: v?.id, datatype: 'wikibase-item' } }
});
} else {
userDefinedClaims.push({ property: { id: prop.id, label: prop.label }, datatype: prop.datatype });
}
} else {
// all statements are "_" placeholders → user fills it
userDefinedClaims.push({ property: { id: prop.id, label: prop.label }, datatype: prop.datatype });
}
}
return { key: slugify(label), label, prefilledClaims, userDefinedClaims };
}
async function buildWbTemplatesFromQuery(query) {
const qids = await searchTemplates(query);
if (!qids.length) return {};
const entities = await fetchEntities(qids);
const propMeta = await fetchPropertyMeta(entities);
const map = {};
Object.values(entities).forEach(e => {
const tpl = transformToTemplate(e, propMeta);
map[tpl.key] = { label: tpl.label, prefilledClaims: tpl.prefilledClaims, userDefinedClaims: tpl.userDefinedClaims };
});
return map;
}
// ---- 4) Build WbTemplates dynamically ----
// If you need to debug, console.log(WbTemplates) after the await.
const WbTemplates = await buildWbTemplatesFromQuery(SEARCH_QUERY);
// Expose globally so your existing helpers can access it
window.WbTemplates = WbTemplates;
// =========================
// YOUR EXISTING FUNCTIONS
// (left untouched, just moved below the builder so they can use WbTemplates)
// =========================
function statementExists(property, value) {
items.toArray().some(item => !!$(item).data().propertyId && $(item).data().statementgroupview.options.value.getKey() === property &&
$(item).data().statementgroupview.options.value.getItemContainer().toArray().some(claimItem =>
claimItem.getClaim().getMainSnak().getValue().getSerialization() === value));
}
function getPropertyEntity(property) {
return {
"id": `${property.id}`,
"title": `Property:${property.id}`,
"datatype": property.datatype,
"label": property.label,
};
}
function prefillStatement(claim, onlyProperty) {
if (onlyProperty) {
pendingStatement(claim.property);
return;
}
let statementListView = $(".wikibase-statementgrouplistview").first().data().wikibaseStatementgrouplistview;
statementListView.enterNewItem();
let items = statementListView.listview.items();
let item = items.last().data();
let sv = item.statementgroupview.statementlistview._listview.items().first().data().statementview;
let snak = sv.$mainSnak.data().snakview;
let es = getPropertyEntity(claim.property);
let selector = snak._getPropertySelector();
selector.element.val(es.label);
selector.element.attr("title", es.label);
selector._trigger('change');
selector._select(es);
snak._variation._valueView.element.one(
snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
function () {
let valSelector = snak._variation._valueView._expert.$input.data().entityselector;
let valval = getPropertyEntity(claim.value.mainVal);
valSelector.element.val(valval.label);
valSelector._trigger("change");
valSelector._select(valval);
if (!!claim.qualifiers) {
let qlistview = sv._qualifiers;
qlistview.enterNewItem();
var qsnaklistview = qlistview.value()[qlistview.value().length - 1];
qsnaklistview.enterNewItem();
let qslv = $(sv._qualifiers.items().first()).data().snaklistview;
let qsnak = qslv._listview.items().first().data().snakview;
let qsel = qsnak._getPropertySelector();
qsel.element.val(es.label);
qsel._trigger('change');
qsel._select(es);
qsnak._variation._valueView.element.one(
qsnak._variation._valueView.widgetEventPrefix + 'afterstartediting',
function () {
let qvalSelector = qsnak._variation._valueView._expert.$input.data().entityselector;
let qvalval = getPropertyEntity(claim.value.mainVal);
qvalSelector.element.val(qvalval.label);
qvalSelector._trigger("change");
valSelector._trigger("change");
qvalSelector._select(qvalval);
}
);
qsnak._variation._valueView.element.one(
qsnak._variation._valueView.widgetEventPrefix + 'change',
function () {
let toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
toolbar._controller.stopEditing(false);
});
}
}
);
snak._variation._valueView.element.one(
snak._variation._valueView.widgetEventPrefix + 'change',
function () {
let toolbar = item.statementgroupview.statementlistview._listview.items().last().data().edittoolbar;
toolbar._controller.stopEditing(false);
});
}
function pendingStatement(claim) {
let prop = claim.property;
return new Promise((resolve, reject) => {
let statementListView = $(".wikibase-statementgrouplistview")
.first()
.data().wikibaseStatementgrouplistview;
statementListView.enterNewItem();
let items = statementListView.listview.items();
let item = items.last().data();
let sv = item.statementgroupview.statementlistview._listview.items();
let snak = $(sv).data().statementview.$mainSnak.data().snakview;
let selector = snak._getPropertySelector();
// Set up event listeners and chain them to resolve when finished.
selector.element.on('entityselectorselected', (event, entityId) => {
$(snak._variation).on("afterdraw", function () {
snak._variation._valueView.element.one(
snak._variation._valueView.widgetEventPrefix + 'afterstartediting',
function () {
if (claim.datatype === "wikibase-item") {
let valSelector = snak._variation._valueView._expert.$input.data().entityselector;
let jqe = snak._variation._valueView._expert.$input.data().jqueryEventSpecialEachchange;
valSelector.element.val("temp");
valSelector._trigger("change");
valSelector._select({ id: "Q80", datatype: "wikibase-item" });
jqe.handlers[0].call();
jqe.handlers[1].call();
valSelector.element.val("");
valSelector._trigger("change");
jqe.handlers[0].call();
jqe.handlers[1].call();
}
else {
let valSelector = snak._variation._valueView._expert.$input.data().inputautoexpand;
let jqe = snak._variation._valueView._expert.$input.data().jqueryEventSpecialEachchange;
if (claim.datatype === "globe-coordinate") {
valSelector.$input.val("0,0");
jqe.handlers[0].call();
jqe.handlers[1].call();
}
else {
valSelector.$input.val("__"); // shows the placeholder behavior
jqe.handlers[0].call();
jqe.handlers[1].call();
}
valSelector.$input.val("");
jqe.handlers[0].call();
jqe.handlers[1].call();
}
// Once processing is complete, resolve the Promise.
resolve();
}
);
});
});
selector.element.val(prop.label);
selector._trigger('change');
selector._select(prop);
});
}
function addDetails(templateName) {
var claims = WbTemplates[templateName].userDefinedClaims;
var chain = Promise.resolve();
claims.forEach(function (claim) {
chain = chain.then(function () {
return pendingStatement(claim);
});
});
return chain;
}
function applyTemplate(templateName) {
WbTemplates[templateName].prefilledClaims.forEach(claim => {
prefillStatement(claim, false);
});
}
// ---- 5) UI: render only after WbTemplates is ready ----
function initTemplatingUI() {
// If nothing came back, don't render the UI
if (!WbTemplates || !Object.keys(WbTemplates).length) {
console.warn('No templates found for query:', SEARCH_QUERY);
return;
}
$(".wikibase-entitytermsview").append(`
<div class="wikibase-templating-section">
<div class="wikibase-templating-section-title"><span class="magic-wand-icon"></span> RDA Entity template</div>
<div class="wikibase-templating-help"><span class="settings-text">Select an RDA entity class to apply predefined template entries and additional entity details.</span></div>
<div class="wikibase-templating-form oo-ui-labelElement oo-ui-fieldLayout">
<div class=""><label for="wbtemplate">Entity class</label></div>
<div>
<select id="wbtemplate" class="cdx-select"></select>
</div>
</div>
<div class="mw-htmlform-submit-buttons">
<button id="applyTemplateBtn" class="cdx-button">Apply template</button>
<button id="addDetailsBtn" class="cdx-button">Add details</button>
</div>
</div>
`);
// Populate selector
Object.entries(WbTemplates).forEach(([key, tpl]) => {
$('#wbtemplate').append($('<option>', { value: key, text: tpl.label }));
});
// Wire buttons
$("#applyTemplateBtn").on("click", function () {
applyTemplate($("#wbtemplate").find(":selected").val());
});
$("#addDetailsBtn").on("click", function () {
addDetails($("#wbtemplate").find(":selected").val());
});
}
// finally render
initTemplatingUI();
})().catch(console.error);
});