User:Techwizzie/forage.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.
/**
* Forage - JavaScript script that provides a user-friendly interface for adding data in Wikidata
* or (with some modifications) any other Wikibase instance.
*
* Development of this tool is done at https://github.com/sanjay-thiyagarajan/forage
*
* @author Sanjay Thiyagaran
* @author Naresh Kumar
* @author Yaron Koren
* @license MIT
*/
messages = {
en: {
"menu-title": "Add property values",
"fields-for-class-title": "Fields for class",
"other-fields-title": "Other fields",
"external-ids-title": "External IDs",
"ids-for-class-title": "IDs for class",
"other-ids-title": "Other IDs",
},
};
mw.messages.set(messages["en"]);
var lang = mw.config.get("wgUserLanguage");
if (lang && lang != "en" && lang in messages) {
mw.messages.set(messages[lang]);
}
mw.loader.using("@wikimedia/codex").then(function (require) {
const Vue = require("vue");
const Codex = require("@wikimedia/codex");
const instanceOfItemID = "P31";
const propertiesForTypeID = "P1963";
const subClassOfID = "P279";
const pointInTimeID = "P585";
const startTimeID = "P580";
const endTimeID = "P582";
var statementsMap = {};
var newStatementsMap = {};
var allProperties = [];
var allPropertyLabels = [];
var allClassLabels = {};
var statementsToCreate = [];
var propertiesForClasses = {};
var parentClassIDs = [];
var allPropIDLabelsMap = {
"": "",
};
var propertyDatatypeMap = {};
var classIDList = [];
var allUnitIDs = new Set();
var menuList = $("#right-navigation").find(".vector-menu-content-list");
var editEntityDiv = $("" ).attr({
id: "ca-edit-entity",
class: "vector-tab-noicon mw-list-item",
});
var editFieldSpan = $("").text(mw.msg("menu-title"));
var anchor = $("").append(editFieldSpan);
anchor.on("click", function () {
var wbrepo = mw.config.get("wbRepo");
window.location.href =
wbrepo.url +
wbrepo.articlePath.replace("$1", mw.config.get("wgTitle")) +
"?addpropvalues";
});
if (mw.config.get("wgPageContentModel") === "wikibase-item") {
editEntityDiv.append(anchor);
menuList.append(editEntityDiv);
const queryString = window.location.search;
if (queryString.includes("?addpropvalues")) {
// Active tab switching
menuList.find("li").each(function () {
$(this).removeClass("selected");
});
$("#ca-edit-entity").addClass("selected");
// Function to retrieve parent classes, via the "instance of" property
function getParentClassIDs(entity) {
var entityParentClassIDs = [];
if (entity.claims[instanceOfItemID]) {
entity.claims[instanceOfItemID].forEach(function (val) {
entityParentClassIDs.push(val.mainsnak.datavalue.value.id);
});
}
return entityParentClassIDs;
}
// Function to retrieve values map
function setStatementsMap(entity, propertyIDs) {
propertyIDs.forEach(function (propertyID) {
if (entity.claims[propertyID]) {
statementsMap[propertyID] = entity.claims[propertyID];
}
});
}
async function getParentClasses(parentClassIDs) {
if (parentClassIDs.length === 0) {
return [];
}
let newIds = parentClassIDs.filter(function (classID) {
return !classIDList.includes(classID);
});
classIDList = classIDList.concat(newIds);
const api = new mw.Api();
const requestParams = {
action: "wbgetentities",
ids: newIds,
props: "claims",
format: "json",
};
try {
const res = await api.get(requestParams);
var parentIDs = new Set();
for (const itemID of newIds) {
if (res.entities[itemID].claims[subClassOfID] !== undefined) {
const parentClasses = res.entities[itemID].claims[subClassOfID];
parentClasses.forEach(async function (classValue) {
if (
classValue.mainsnak.snaktype !== "novalue" &&
classValue.rank !== "deprecated"
) {
parentIDs.add(classValue.mainsnak.datavalue.value.id);
}
});
}
}
await getParentClasses(Array.from(parentIDs));
} catch (error) {
console.error("API call failed: ", error);
}
}
// Helper function to chunk an array into smaller arrays
function chunkArray(array, size) {
return Array.from(
{ length: Math.ceil(array.length / size) },
function (_, index) {
return array.slice(index * size, index * size + size);
}
);
}
// Function to retrieve class properties from Wikidata API
function retrieveClassProperties(
api,
requestParams,
parentClassIDs,
entity
) {
return new Promise(async function (resolve, reject) {
var parentClassIDBatches = chunkArray(parentClassIDs, 50);
var promises = parentClassIDBatches.map(function (classIDBatch) {
return new Promise(function (innerResolve, innerReject) {
var requestParams = {
action: "wbgetentities",
ids: classIDBatch,
props: "claims",
format: "json",
};
let typesResponse = api.get(requestParams);
typesResponse.done(function (res) {
let entities = res.entities;
let allPropIDs = new Set();
classIDBatch.forEach(function (classID) {
let curClass = entities[classID];
if (!propertiesForClasses[classID]) {
propertiesForClasses[classID] = [];
}
if (curClass.claims[propertiesForTypeID]) {
curClass.claims[propertiesForTypeID].forEach(function (curProperty) {
var snakType = curProperty.mainsnak.snaktype;
if (snakType == "novalue" || snakType == "somevalue") {
return;
}
let propertyID = curProperty.mainsnak.datavalue.value.id;
if (allPropIDs.has(propertyID)) {
return;
}
propertiesForClasses[classID].push(propertyID);
allPropIDs.add(propertyID);
});
}
});
let propertyIDsForCurrentClassBatch = Array.from(allPropIDs);
// Remove "instance of" value(s)
propertyIDsForCurrentClassBatch.splice(
propertyIDsForCurrentClassBatch.indexOf(instanceOfItemID),
1
);
setStatementsMap(entity, propertyIDsForCurrentClassBatch);
retrieveLabels(
api,
propertyIDsForCurrentClassBatch,
classIDBatch
).then(function (labelsResult) {
allProperties = allProperties.concat(
labelsResult.allProperties
);
Object.assign(allClassLabels, labelsResult.allClassLabels);
innerResolve();
});
});
});
});
var propertyIDsForThisPage = Object.keys(entity.claims);
// Remove "instance of" value(s)
propertyIDsForThisPage.splice(
propertyIDsForThisPage.indexOf(instanceOfItemID),
1
);
setStatementsMap(entity, propertyIDsForThisPage);
var batchedPropertyIDsForThisPage = chunkArray(
propertyIDsForThisPage,
50
);
var propertyLabelPromises = batchedPropertyIDsForThisPage.map(
function (curPropertyIDsBatch) {
return new Promise(async function (innerResolve, innerReject) {
var requestParams = {
action: "wbgetentities",
ids: curPropertyIDsBatch,
props: "labels",
languages: lang + "|en",
format: "json",
};
var result = await api.get(requestParams);
allProps = Object.entries(result.entities).map(function ([
propID,
value,
]) {
propertyDatatypeMap[propID] = value.datatype;
//let propertyIDs = Object.keys(value.claims);
//setStatementsMap(value, propertyIDs);
var propLabel;
if (value.labels && value.labels[lang]) {
propLabel = value.labels[lang].value;
} else if (value.labels && value.labels["en"]) {
propLabel = value.labels["en"].value;
} else {
propLabel = propID;
}
return {
id: propID,
datatype: value.datatype,
label: propLabel,
};
});
allProperties = allProperties.concat(allProps);
innerResolve();
});
}
);
// Wait for all promises to resolve and process the results
Promise.all(promises)
.then(function (_) {
Promise.all(propertyLabelPromises)
.then(function (_) {
resolve();
})
.catch(function (error) {
console.error("Error fetching labels:", error);
});
})
.catch(function (error) {
console.error("Error fetching labels:", error);
});
});
}
function fetchPropertyLabels(api, properties) {
var lang = mw.config.get("wgUserLanguage");
var requestParams = {
action: "wbgetentities",
props: "labels",
languages: lang + "|en",
format: "json",
};
// Group properties into batches of 50 or fewer
var propertyBatches = chunkArray(properties, 50);
return new Promise(function (resolve, reject) {
// Create promises for each batch and make asynchronous requests
var promises = propertyBatches.map(function (propertyBatch) {
var propertyBatchIDs = [];
propertyBatch.map(function (prop) {
propertyBatchIDs.push(prop.id);
});
// Fetch labels for the current batch of properties
var labelsResponse = api.get(
$.extend({}, requestParams, {
ids: propertyBatchIDs,
})
);
return labelsResponse.then(function (res) {
var allItemData = res.entities;
var properties = [];
// Process each property in the batch
Object.keys(allItemData).forEach(function (itemID) {
var curItemData = allItemData[itemID];
var propLabel;
if (curItemData.labels && curItemData.labels[lang]) {
propLabel = curItemData.labels[lang].value;
} else if (curItemData.labels && curItemData.labels["en"]) {
propLabel = curItemData.labels["en"].value;
} else {
propLabel = curItemData.id;
}
properties.push({
id: curItemData.id,
datatype: curItemData.datatype,
label: propLabel,
});
});
return {
properties: properties,
};
});
});
// Wait for all promises to resolve and process the results
Promise.all(promises)
.then(function (results) {
var props = [];
// Merge the results
results.forEach(function (result) {
props = props.concat(result.properties);
});
allPropertyLabels = props;
resolve();
})
.catch(function (error) {
console.error("Error fetching labels:", error);
});
});
}
// Function to retrieve labels from Wikidata API
async function retrieveLabels(api, propertyIDs, classIDs) {
var lang = mw.config.get("wgUserLanguage");
var requestParams = {
action: "wbgetentities",
props: "labels",
languages: lang + "|en",
format: "json",
};
var batchSize = 50; // Maximum number of IDs per request
var chunks = [];
var i, j;
// Split property and class IDs into chunks of batchSize
for (i = 0, j = propertyIDs.length; i < j; i += batchSize) {
chunks.push(propertyIDs.slice(i, i + batchSize));
}
for (i = 0, j = classIDs.length; i < j; i += batchSize) {
chunks.push(classIDs.slice(i, i + batchSize));
}
// Create promises for each chunk and make asynchronous requests
var promises = chunks.map(function (chunk) {
var labelsResponse = api.get(
$.extend({}, requestParams, { ids: chunk })
);
return labelsResponse.then(function (res) {
var allItemData = res.entities;
var properties = [];
var classLabels = {};
Object.keys(allItemData).forEach(function (itemID) {
var curItemData = allItemData[itemID];
var curClassLabel;
if (curItemData.labels && curItemData.labels[lang]) {
curClassLabel = curItemData.labels[lang].value;
} else if (lang !== "en" && curItemData.labels && curItemData.labels["en"]) {
curClassLabel = curItemData.labels["en"].value;
} else {
curClassLabel = curItemData.id;
}
if (classIDs.indexOf(curItemData.id) === -1) {
properties.push({
id: curItemData.id,
datatype: curItemData.datatype,
label: curClassLabel,
});
} else {
classLabels[curItemData.id] = curClassLabel;
}
propertyDatatypeMap[curItemData.id] = curItemData.datatype;
});
return { properties: properties, classLabels: classLabels };
});
});
// Wait for all promises to resolve and process the results
try {
const results = await Promise.all(promises);
var allProperties = [];
var allClassLabels = {};
// Merge the results
results.forEach(function (result) {
allProperties = allProperties.concat(result.properties);
Object.assign(allClassLabels, result.classLabels);
});
return { allProperties, allClassLabels };
} catch (error) {
console.error("Error fetching labels:", error);
}
}
function deleteValue(idx, propID) {
this.newStatementsMap[propID].splice(idx, 1);
}
/**
* Format search results for consumption by TypeaheadSearch.
*
* @param pages
* @return
*/
function adaptApiResponse(pages) {
return pages.map(
({
id,
label,
url,
match,
description,
display = {},
thumbnail,
}) => ({
value: id,
label,
match: match.type === "alias" ? `(${match.text})` : "",
description,
language: {
label: display && display.label && display.label.language,
match: match.type === "alias" ? match.language : undefined,
description:
display && display.description && display.description.language,
},
thumbnail: thumbnail
? {
url: thumbnail.url,
width: thumbnail.width,
height: thumbnail.height,
}
: undefined,
})
);
}
function parseTimeValue(timeValue) {
var timeString = timeValue.time.replace("+", "");
var isBC = false;
if (timeString.startsWith('-')) {
isBC = true;
timeString = timeString.substring(1);
}
var precision = timeValue.precision;
// We really only worry about three precisions: full date (11),
// year and month (10), and year-only (9) - if it's more precise
// than a date, just display the date, and if it's less precise
// than a year, just display whatever year was set.
if (precision >= 11) {
let dateObj = new Date(timeString);
let year = dateObj.getUTCFullYear();
if (isBC) {
year = '-' + year;
}
let monthNum = dateObj.getUTCMonth() + 1;
let monthNames = mw.config.get("wgMonthNames");
let dayNum = dateObj.getUTCDate();
return dayNum + ' ' + monthNames[monthNum] + ' ' + year;
} else if (precision == 10) {
// JS date parsing won't work on a date that looks like "YYYY-MM-00",
// so instead just get the digits manually.
let matches = timeString.match(/(\d+)-(\d+)/);
// Remove trailing zeros, if it's a less-than-four-digit year.
let year = matches[1].replace(/^0+/,"");
if (isBC) {
year = '-' + year;
}
let monthNum = parseInt(matches[2]);
let monthNames = mw.config.get("wgMonthNames");
return monthNames[monthNum] + ' ' + year;
} else {
// JS date parsing won't work on a date that looks like "YYYY-00-00",
// so instead just get the first group of digits.
let matches = timeString.match(/(\d+)/);
// Remove trailing zeros, if it's a less-than-four-digit year.
let year = matches[0].replace(/^0+/,"");
if (isBC) {
// @TODO - it would be good to instead display "BCE" here, or its equivalent in other
// languages, but that requires access to the i18n message "wikibase-time-precision-BCE",
// which we don't have.
year = '-' + year;
}
return year;
}
}
function parseValue(statement) {
var curValue = statement.mainsnak.datavalue.value;
var str;
if (curValue.id) {
// entity
str = allPropIDLabelsMap[curValue.id];
if (str == "") {
str = curValue.id;
}
let wbrepo = mw.config.get("wbRepo");
let entityURL =
wbrepo.url + wbrepo.articlePath.replace("$1", curValue.id);
str = ' + entityURL + '" target="_blank">' + str + "";
} else if (curValue.amount) {
// number or quantity
// Remove "+" if it's there, and add thousands separators.
// @TODO - this should use the MediaWiki settings, not the browser locale.
str = Number(curValue.amount).toLocaleString(undefined, {
maximumFractionDigits: 15,
});
if (curValue.unit !== "1") {
// quantity
var unitID = curValue.unit.split("/").pop(); // curValue is a URL.
// Add an HTML tag for this ID - it will be replaced with the label later.
str += " <" + unitID + ">" + unitID + ">";
allUnitIDs.add(unitID);
}
} else if (curValue.time) {
// time
str = parseTimeValue(curValue);
} else if (curValue.latitude && curValue.longitude) {
// coordinates ("globe-coordinate")
str = "(" + curValue.latitude + ", " + curValue.longitude + ")";
str =
' +
curValue.latitude +
"_N_" +
curValue.longitude +
'_E" class="external" target="_blank">' +
str +
"";
} else if (curValue.text) {
// monolingual text
str = curValue.text;
if (curValue.language) {
str += " (" + curValue.language + ")";
}
} else if (statement.mainsnak.datatype == "commonsMedia") {
// file
let wbrepo = mw.config.get("wbRepo");
let entityURL =
wbrepo.url + wbrepo.articlePath.replace("$1", "File:" + curValue);
str =
' + entityURL + '" target="_blank">' + curValue + "";
} else if (statement.mainsnak.datatype == "url") {
str =
' +
curValue +
'" class="external" target="_blank">' +
curValue +
"";
} else {
// string, external ID etc. - or just a malformed value?
str = curValue;
}
// If there's a time-based qualifier for this value, display that/those within parentheses.
if (statement.qualifiers) {
var pointInTimeQualifiers = statement.qualifiers[pointInTimeID]
if (pointInTimeQualifiers && pointInTimeQualifiers[0].datavalue) {
var pointInTime =
parseTimeValue(pointInTimeQualifiers[0].datavalue.value);
str += " (" + pointInTime + ")";
} else if (
statement.qualifiers[startTimeID] ||
statement.qualifiers[endTimeID]
) {
var startTimeQualifiers = statement.qualifiers[startTimeID];
var startTime = (startTimeQualifiers && startTimeQualifiers[0].datavalue)
? parseTimeValue(startTimeQualifiers[0].datavalue.value)
: "";
var endTimeQualifiers = statement.qualifiers[endTimeID];
var endTime = (endTimeQualifiers && endTimeQualifiers[0].datavalue)
? parseTimeValue(endTimeQualifiers[0].datavalue.value)
: "";
str += " (" + startTime + " - " + endTime + ")";
}
}
return str;
}
// Function to retrieve labels from Wikidata API
function fetchEntityAndQualifierLabels(api, entityIDs) {
var lang = mw.config.get("wgUserLanguage");
var batchSize = 50; // Maximum number of IDs per request
var chunks = [];
var requestParams = {
action: "wbgetentities",
props: "labels",
languages: lang + "|en",
format: "json",
};
var i, j;
// Split entityIDs into chunks of batchSize
for (i = 0, j = entityIDs.length; i < j; i += batchSize) {
chunks.push(entityIDs.slice(i, i + batchSize));
}
// Create promises for each chunk and make asynchronous requests
var promises = chunks.map(function (chunk) {
var labelsResponse = api.get(
$.extend({}, requestParams, { ids: chunk })
);
return labelsResponse.then(function (res) {
var allItemData = res.entities;
var labels = {};
Object.keys(allItemData).forEach(function (itemID) {
var curItemData = allItemData[itemID];
if (curItemData.labels && curItemData.labels[lang]) {
labels[curItemData.id] = curItemData.labels[lang].value
} else if (lang != "en" && curItemData.labels && curItemData.labels["en"] ) {
labels[curItemData.id] = curItemData.labels["en"].value;
} else {
labels[curItemData.id] = itemID;
}
});
return { propIDLabelMap: labels };
});
});
// Wait for all promises to resolve and process the results
return Promise.all(promises)
.then(function (results) {
// Merge the results
results.forEach(function (result) {
Object.assign(allPropIDLabelsMap, result.propIDLabelMap);
});
return allPropIDLabelsMap;
})
.catch(function (error) {
console.error("Error fetching labels:", error);
});
}
/**
* For each unit used on this page, get its label and replace the (hacky) HTML
* tag created for it with the label.
*/
function replaceUnitIDsWithLabels() {
if (allUnitIDs.size == 0) {
return;
}
var api = new mw.Api();
var lang = mw.config.get("wgUserLanguage");
var requestParams = {
action: "wbgetentities",
ids: Array.from(allUnitIDs),
props: "labels",
languages: lang + "|en",
format: "json",
};
var result = api.get(requestParams);
result.done(async function (res) {
var unitEntities = res.entities;
Object.keys(unitEntities).forEach(function (unitID) {
var unitLabel;
if (unitEntities[unitID].labels && unitEntities[unitID].labels[lang]) {
unitLabel = unitEntities[unitID].labels[lang].value;
} else if (unitEntities[unitID].labels && unitEntities[unitID].labels["en"]) {
unitLabel = unitEntities[unitID].labels["en"].value;
} else {
unitLabel = unitID;
}
$(unitID).replaceWith(unitLabel);
});
});
}
function capitalizeFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
Vue.createMwApp({
data: function () {
return {
name: "SimplyEdit",
classDivStyle:
"border: 1px solid #b0b8bf; width: 80%; padding: 0 1rem 1rem 1rem; margin-top: 1rem; background: aliceblue;",
externalIDDivStyle:
"font-size: 14px; border: 1px solid #aaa; width: 80%; padding: 0 1rem 1rem 1rem; margin-top: 1rem; background: #f5f5f5;",
classHeaderStyle: "font-size: 15px;font-weight: bold;",
qualifierLabelStyle: "font-size: 12px; text-decoration: italic;",
valueInputStyle:
"display: flex; flex-direction: row; margin: 5px 0 10px 0;",
valueTagStyle:
"display: inline-block; font-size: 13px; padding: 3px 10px; border: 1px solid #999; background: lightyellow; border-radius: 10px; margin: 0 8px 6px 0;",
classIDs: [],
classLabels: {},
properties: [],
classPropertiesMap: {},
otherPropertiesMap: {},
statementsMap: {},
progress: true,
autocompleteItems: [],
existingValueLabels: {},
newStatementsMap: {},
mw,
allPropIDLabelsMap,
deleteValue,
parseValue,
message: {
show: false,
state: 'success',
text: ''
},
monthNames: mw.config.get('wgMonthNames'),
publishMsg: "✔ " + mw.msg('wikibase-publish'),
cancelMsg: "✘ " + mw.msg('wikibase-cancel'),
unitMsg: mw.msg('valueview-expertextender-unitsuggester-label'),
dayMsg: capitalizeFirst(mw.msg('valueview-expert-timeinput-precision-day')) + ':',
monthMsg: capitalizeFirst(mw.msg('valueview-expert-timeinput-precision-month')) + ':',
yearMsg: capitalizeFirst(mw.msg('valueview-expert-timeinput-precision-year')) + ':',
languageMsg: mw.msg('valueview-expertextender-languageselector-label')
};
},
template: `
v-if="message.show"
:type="message.state"
dismiss-button-label="Close"
:fade-in="true"
:auto-dismiss="true"
:display-time="3000"
style="position: fixed; right: 2%; width: 70%;"
>
{{message.text}}
{{mw.msg('fields-for-class-title')}} {{classLabels[classID]}}
{{properties[propID].label}}
+
id="'typeahead-search-' + idx"
v-if="properties[propID].datatype === 'wikibase-item' && !(statement.references || statement.qualifiers) && (statement.mainsnak.snaktype !== 'novalue')"
:initial-input-value="allPropIDLabelsMap[statement.mainsnak.datavalue.value.id]"
placeholder="Type or choose an option"
search-results-label="Search results"
:search-results="autocompleteItems"
:show-thumbnail="true"
:highlight-query="true"
:visible-item-limit="5"
@input="comboboxOnChange"
@search-result-click="comboboxOnSelect($event, propID, idx)"
@blur="resetOptions"
>
v-if="(properties[propID].datatype === 'commonsMedia' || properties[propID].datatype === 'string') && !(statement.references || statement.qualifiers) && (statement.mainsnak.snaktype !== 'novalue')"
v-model="statement.mainsnak.datavalue.value"
>
v-model="statement.mainsnak.datavalue.value.amount"
>
{{unitMsg}}
placeholder="Type or choose an option"
search-results-label="Search results"
:search-results="autocompleteItems"
:highlight-query="true"
:visible-item-limit="5"
@input="comboboxOnChange"
@search-result-click="unitComboboxOnSelect($event, propID, idx)"
@blur="resetOptions"
>
{{publishMsg}}
{{cancelMsg}}
{{mw.msg('other-fields-title')}}
{{properties[propID].label}}
+
id="'typeahead-search-' + idx"
v-if="properties[propID].datatype === 'wikibase-item' && !(statement.references || statement.qualifiers) && (statement.mainsnak.snaktype !== 'novalue')"
:initial-input-value="allPropIDLabelsMap[statement.mainsnak.datavalue.value.id]"
placeholder="Type or choose an option"
search-results-label="Search results"
:search-results="autocompleteItems"
:show-thumbnail="true"
:highlight-query="true"
:visible-item-limit="5"
@input="comboboxOnChange"
@search-result-click="comboboxOnSelect($event, propID, idx)"
@blur="resetOptions"
>
v-if="(properties[propID].datatype === 'commonsMedia' || properties[propID].datatype === 'string') && !(statement.references || statement.qualifiers) && (statement.mainsnak.snaktype !== 'novalue')"
v-model="statement.mainsnak.datavalue.value"
>
v-model="statement.mainsnak.datavalue.value.amount"
>
{{unitMsg}}
placeholder="Type or choose an option"
search-results-label="Search results"
:search-results="autocompleteItems"
:highlight-query="true"
:visible-item-limit="5"
@input="comboboxOnChange"
@search-result-click="unitComboboxOnSelect($event, propID, idx)"
@blur="resetOptions"
>
{{publishMsg}}
{{cancelMsg}}
{{mw.msg('external-ids-title')}}
{{mw.msg('ids-for-class-title')}} {{classLabels[classID]}}
{{properties[propID].label}}
+
{{parseValue(statement)}}
`,
mounted() {
$("title").prepend(mw.msg("menu-title") + ": ");
$(".wikibase-title").prepend(mw.msg("menu-title") + ":");
const itemID = mw.config.get("wbEntityId");
var api = new mw.Api();
var requestParams = {
action: "wbgetentities",
ids: itemID,
props: "claims",
format: "json",
};
var result = api.get(requestParams);
const that = this;
result.done(async function (res) {
var entity = res.entities[itemID];
parentClassIDs = getParentClassIDs(entity);
await getParentClasses(parentClassIDs);
that.classIDs = classIDList;
await retrieveClassProperties(
api,
requestParams,
classIDList,
entity
);
that.classLabels = allClassLabels;
await fetchPropertyLabels(api, allProperties);
that.properties = {};
allPropertyLabels.forEach(function (obj) {
that.properties[obj.id] = obj;
});
var newClassPropertiesMap = {};
var newOtherPropertiesMap = {
general: [],
external: [],
};
const allGenerals = allPropertyLabels
.filter(function (obj) {
return obj.datatype !== "external-id";
})
.map(function (obj) {
return obj.id;
});
const allExternals = allPropertyLabels
.filter(function (obj) {
return obj.datatype === "external-id";
})
.map(function (obj) {
return obj.id;
});
classIDList.forEach(function (classID) {
if (propertiesForClasses[classID]) {
newClassPropertiesMap[classID] = {};
newClassPropertiesMap[classID]["general"] =
propertiesForClasses[classID].filter(function (propID) {
return allGenerals.includes(propID);
});
newClassPropertiesMap[classID]["external"] =
propertiesForClasses[classID].filter(function (propID) {
return allExternals.includes(propID);
});
// Create alphabetically sorted (by property label) arrays as well.
let generalPropertiesArray = Object.values(
newClassPropertiesMap[classID]["general"]
);
newClassPropertiesMap[classID]["generalSorted"] =
generalPropertiesArray.sort((a, b) => {
return that.properties[a].label.toLowerCase() >
that.properties[b].label.toLowerCase()
? 1
: -1;
});
let externalPropertiesArray = Object.values(
newClassPropertiesMap[classID]["external"]
);
newClassPropertiesMap[classID]["externalSorted"] =
externalPropertiesArray.sort((a, b) => {
return that.properties[a].label.toLowerCase() >
that.properties[b].label.toLowerCase()
? 1
: -1;
});
}
});
var existingGeneralPropertyIDs = [];
var existingExternalPropertyIDs = [];
Object.keys(newClassPropertiesMap).forEach(function (classID) {
existingGeneralPropertyIDs = existingGeneralPropertyIDs.concat(
newClassPropertiesMap[classID]["general"]
);
existingExternalPropertyIDs = existingExternalPropertyIDs.concat(
newClassPropertiesMap[classID]["external"]
);
});
allPropertyLabels.forEach(function (prop) {
if (prop.datatype !== "external-id") {
if (!existingGeneralPropertyIDs.includes(prop.id)) {
newOtherPropertiesMap["general"].push(prop.id);
}
} else {
if (!existingExternalPropertyIDs.includes(prop.id)) {
newOtherPropertiesMap["external"].push(prop.id);
}
}
});
// Create alphabetically sorted (by property label) arrays as well.
let generalPropertiesArray = Object.values(
newOtherPropertiesMap["general"]
);
newOtherPropertiesMap["generalSorted"] =
generalPropertiesArray.sort((a, b) => {
return that.properties[a].label.toLowerCase() >
that.properties[b].label.toLowerCase()
? 1
: -1;
});
let externalPropertiesArray = Object.values(
newOtherPropertiesMap["external"]
);
newOtherPropertiesMap["externalSorted"] =
externalPropertiesArray.sort((a, b) => {
return that.properties[a].label.toLowerCase() >
that.properties[b].label.toLowerCase()
? 1
: -1;
});
var allQualifiers = new Set();
var allEntityIDs = new Set();
Object.keys(statementsMap).forEach(function (propID) {
var qualifiers = new Set();
statementsMap[propID].forEach(function (statement) {
if (statement.qualifiers) {
Object.keys(statement.qualifiers).forEach(function (
qualifierID
) {
qualifiers.add(qualifierID);
allQualifiers.add(qualifierID);
});
}
if (
statement.mainsnak.datatype === "wikibase-item" &&
statement.mainsnak.snaktype !== "novalue" &&
statement.mainsnak.snaktype !== "somevalue"
) {
allEntityIDs.add(statement.mainsnak.datavalue.value.id);
}
});
});
allQualifiers = Array.from(allQualifiers);
allEntityIDs = Array.from(allEntityIDs);
const allIDs = allQualifiers.concat(allEntityIDs);
await fetchEntityAndQualifierLabels(api, allIDs);
that.otherPropertiesMap = newOtherPropertiesMap;
that.classPropertiesMap = newClassPropertiesMap;
that.statementsMap = statementsMap;
that.newStatementsMap = newStatementsMap;
that.progress = false;
});
},
updated() {
replaceUnitIDsWithLabels();
},
methods: {
getWikibaseURL: function (id) {
var wbrepo = mw.config.get("wbRepo");
return wbrepo.url + wbrepo.articlePath.replace("$1", id);
},
getWikibasePropertyURL: function (propID) {
return this.getWikibaseURL("Property:" + propID)
},
addNewValue: function (propID) {
const propDataType = propertyDatatypeMap[propID];
var statement = {
mainsnak: {
snaktype: "value",
property: propID,
datavalue: {},
datatype: propDataType,
},
id: "",
};
if (propDataType === "wikibase-item") {
statement.mainsnak.datavalue = {
value: {
"entity-type": "item",
"numeric-id": null,
id: "",
},
};
} else if (
propDataType === "string" ||
propDataType === "commonsMedia" ||
propDataType === "external-id"
) {
statement.mainsnak.datavalue = {
value: "",
};
} else if (propDataType === "quantity") {
statement.mainsnak.datavalue = {
value: {
amount: "",
unit: "1",
},
};
} else if (propDataType === "time") {
statement.mainsnak.datavalue = {
value: {
time: "",
},
};
} else if (propDataType === "monolingualtext") {
statement.mainsnak.datavalue = {
value: {},
};
} else if (propDataType === "globe-coordinate") {
statement.mainsnak.datavalue = {
value: {
latitude: "",
longitude: "",
},
};
}
if (newStatementsMap[propID]) {
this.newStatementsMap[propID].unshift(statement);
} else {
this.newStatementsMap[propID] = [];
this.newStatementsMap[propID].push(statement);
}
},
comboboxOnChange: function (value) {
this.autocompleteItems = [];
this.$forceUpdate();
var api = new mw.Api();
const lang = mw.config.get("wgUserLanguage");
var requestParams = {
action: "wbsearchentities",
format: "json",
search: value,
language: lang,
type: "item",
limit: 20,
};
var valuesResponse = api.get(requestParams);
const that = this;
valuesResponse.done(function (data) {
// If there are results, format them into an array of
// SearchResults to be passed into TypeaheadSearch for
// display as a menu of search results.
that.autocompleteItems =
data.search && data.search.length > 0
? adaptApiResponse(data.search)
: [];
that.$forceUpdate();
});
},
comboboxOnSelect: function (event, propID, statementIdx) {
const selectedEntityId = event.searchResult.value;
this.newStatementsMap[propID][statementIdx].mainsnak.datavalue.value = {
'entity-type': 'item',
'numeric-id': statementIdx,
'id': selectedEntityId,
'label': event.searchResult.label
};
},
unitComboboxOnSelect: function (event, propID, statementIdx) {
const unitEntityURL = 'http://www.wikidata.org/entity/' + event.searchResult.value;
this.newStatementsMap[propID][statementIdx].mainsnak.datavalue.value.unit = unitEntityURL;
},
submitChanges: async function(event, propID, statementIdx) {
event.target.disabled = true;
const entityID = mw.config.get("wbEntityId");
var api = new mw.Api();
// Fetch CSRF token
var requestParams = {
action: "query",
meta: "tokens",
format: "json"
};
const tokenResponse = await api.get(requestParams);
let dataValue = Vue.toRaw(this.newStatementsMap[propID][statementIdx].mainsnak.datavalue.value);
let dataType = this.newStatementsMap[propID][statementIdx].mainsnak.datatype;
if (dataType == 'time') {
let yearVal = $(event.target).parent().find('.yearInput').val();
// Add a "+" at the beginning (needed for saving) if the year starts
// with a digit (as opposed to "-", or anything else).
if (! isNaN(parseInt(yearVal.charAt(0)))) {
yearVal = '+' + yearVal;
}
dataValue.precision = 9;
let monthVal = $(event.target).parent().find('.monthInput').val();
if (monthVal > 0) dataValue.precision = 10;
if (monthVal.length < 2) monthVal = '0' + monthVal;
let dayVal = $(event.target).parent().find('.dayInput').val();
if (dayVal == '') {
dayVal = '00';
} else {
dataValue.precision = 11;
}
if (dayVal.length < 2) dayVal = '0' + dayVal;
dataValue.time = yearVal + '-' + monthVal + '-' + dayVal + 'T00:00:00Z';
// All of the following are needed for saving - hopefully the calendar model
// (Gregorian) is accurate.
dataValue.timezone = 0;
dataValue.before = 0;
dataValue.after = 0;
dataValue.calendarmodel = 'http://www.wikidata.org/entity/Q1985727';
} else if (dataType == 'monolingualtext') {
dataValue.text = $(event.target).parent().find('.textInput').val();
dataValue.language = $(event.target).parent().find('.langInput').val();
} else if (dataType == 'globe-coordinate') {
dataValue.latitude = $(event.target).parent().find('.latInput').val();
dataValue.longitude = $(event.target).parent().find('.lonInput').val();
// Needed for saving.
dataValue.altitude = null;
dataValue.globe = "http://www.wikidata.org/entity/Q2";
// @TODO - set this based on the numbers entered.
dataValue.precision = 1e-8;
}
const dataLabel = dataValue.label ? dataValue.label : null;
const dataID = dataValue.id ? dataValue.id : null;
requestParams = {
action: "wbcreateclaim",
format: "json",
entity: entityID,
snaktype: "value",
property: propID,
value: JSON.stringify(dataValue),
token: tokenResponse.query.tokens.csrftoken
};
const response = await api.post(requestParams);
event.target.disabled = false;
if(response.success){
if(dataLabel !== null && dataID !== null){
this.allPropIDLabelsMap[dataID] = dataLabel;
}
if (this.statementsMap[propID] == undefined) {
this.statementsMap[propID] = [response.claim];
} else {
this.statementsMap[propID].push(response.claim);
}
this.newStatementsMap[propID].splice(statementIdx, 1);
this.message.state = 'success';
this.message.text = 'Successfully added the new claim';
this.message.show = true;
} else if(response.error){
console.error(resonse.error.code);
this.message.state = 'error';
this.message.text = 'Error saving the new claim: ' + response.error.code;
this.message.show = true;
}
}
},
resetOptions: function (event) {
this.autocompleteItems = [];
this.$forceUpdate();
},
})
.component("cdx-button", Codex.CdxButton)
.component("cdx-typeahead-search", Codex.CdxTypeaheadSearch)
.component("cdx-field", Codex.CdxField)
.component("cdx-progress-bar", Codex.CdxProgressBar)
.component("cdx-text-input", Codex.CdxTextInput)
.component("cdx-accordion", Codex.CdxAccordion)
.component("cdx-message", Codex.CdxMessage)
.mount("#mw-content-text");
}
}
});