User:Hardwigg/wdTableView.js
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)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
;(function() {
const utils = {
// from https://github.com/wikimedia/mediawiki-extensions-Wikibase/blob/587f8a41c359dad8dbc47335ea1968a762063d27/repo/resources/wikibase.ui.entityViewInit.js#L114
getUserLanguages() {
var userLanguages = mw.config.get( 'wbUserSpecifiedLanguages' ),
isUlsDefined = mw.uls && $.uls && $.uls.data,
languages;
if ( !userLanguages.length && isUlsDefined ) {
languages = mw.uls.getFrequentLanguageList().slice( 1, 4 );
} else {
languages = userLanguages.slice();
languages.splice( $.inArray( mw.config.get( 'wgUserLanguage' ), userLanguages ), 1 );
}
languages.unshift( mw.config.get( 'wgUserLanguage' ) );
return languages;
}
};
const listView = {}; {
listView.sgvClass = 'dc-display-list';
{listView.css = `
.wikibase-statementgroupview.dc-display-list .wikibase-statementview {
width: auto;
padding: 0 2mm;
}
.wikibase-statementgroupview.dc-display-list .wikibase-statementview.wb-edit {
width: 100%;
}
`;
}
listView.selected = function($statementGroupView) {
mw.util.addCSS(listView.css);
$statementGroupView.addClass(listView.sgvClass);
}
};
const tableView = {}; {
tableView.sgvClass = 'dc-display-table';
{tableView.css = `
.dc-display-table .wikibase-statementlistview-listview {
position: relative;
display: grid;
grid-template-columns: 1fr;
}
.dc-display-table .wikibase-statementview {
width: auto;
grid-column: 1;
}
.dc-display-table .dc-grid-header {
font-weight: bold;
text-align: center;
padding: 1mm;
grid-row: 1;
position: sticky;
top: 0;
border-top: 1px solid #AAA;
border-bottom: 1px solid #AAA;
background: white;
z-index: 2;
}
.dc-display-table .dc-grid-header~.dc-grid-header {
border-left: 1px solid #AAA;
}
.dc-display-table .dc-add-header {
display: flex;
}
.dc-display-table .dc-add-header.pre-add .dc-add-header-plus {
flex: 1;
}
.dc-display-table .dc-add-header.pre-add input,
.dc-display-table .dc-add-header.pre-add .dc-add-header-x {
display: none;
}
.dc-display-table .dc-add-header.pre-add {
width: 30px;
}
.dc-display-table .dc-add-header.adding {
position: absolute;
top: 0;
left: 0;
right: 0;
padding-left: 10px;
}
.dc-display-table .dc-add-header.adding .dc-add-header-plus {
display: none;
}
.dc-display-table .dc-add-header.adding .dc-add-header-x {
width: 30px;
}
.dc-display-table .dc-add-header.adding input {
flex: 1;
min-weight: 40px;
}
.dc-display-table .dc-extra-cell {
padding: 1mm;
}
.dc-display-table.dc-quals-as-cols .wikibase-statementview:not(.wb-edit) .wikibase-statementview-qualifiers {
display: none;
}
.dc-display-table.dc-quals-as-cols .dc-grid-header.dc-qual-col {
border-left-style: dotted;
}
.dc-display-table .wikibase-statementview-mainsnak {
max-width: none;
}
.dc-display-table .wikibase-statementview-references-container {
display:block;
}
`};
tableView.templates = {}; {
{tableView.templates.DEFAULT_TABLE_HEADERS = `
<div class="dc-grid-header">Statement</div>
<div class="dc-grid-header dc-add-header pre-add">
<a class="dc-add-header-plus">+</a>
<input name="dc-new-col">
<a class="dc-add-header-x" >×</a>
</div>`;}
tableView.templates.NEW_COL_SPARQL = (qid, curPid, itemPid) => {
return `SELECT
?x ?type
?timeTime ?timePrecision ?timeTimezone ?timeCalendarModel
?quantityAmount ?quantityUnit
?monolingualtextText ?monolingualtextLanguage
?itemId
?string
WHERE {
wd:${qid} p:${curPid}/ps:${curPid} ?x.
?x p:${itemPid} ?s.
OPTIONAL {
?s psv:${itemPid} [
a wikibase:TimeValue;
wikibase:timeValue ?timeTime;
wikibase:timePrecision ?timePrecision;
wikibase:timeTimezone ?timeTimezone;
wikibase:timeCalendarModel ?timeCalendarModel;
].
BIND("time" as ?type).
}
OPTIONAL {
?s psv:${itemPid} [
a wikibase:QuantityValue;
wikibase:quantityAmount ?quantityAmount;
wikibase:quantityUnit ?quantityUnit;
].
BIND("quantity" as ?type).
}
OPTIONAL {
?s ps:${itemPid} ?monolingualtextText.
Filter(lang(?monolingualtextText))
BIND(lang(?monolingualtextText) as ?monolingualtextLanguage)
BIND("monolingualtext" as ?type).
}
OPTIONAL {
?s ps:${itemPid} ?itemId.
FILTER(isURI(?itemId))
BIND("item" as ?type).
}
OPTIONAL {
?s ps:${itemPid} ?string.
BIND("string" as ?type).
}
# SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
}`;
}
}
tableView.utils = {
entityFormatter: (function() {
const userLanguages = utils.getUserLanguages();
const repoConfig = mw.config.get( 'wbRepo' );
const repoApiUrl = repoConfig.url + repoConfig.scriptPath + '/api.php';
const mwApi = wb.api.getLocationAgnosticMwApi( repoApiUrl );
const repoApi = new wb.api.RepoApi( mwApi );
const formatterFactory = new wb.formatters.ApiValueFormatterFactory(
new wb.api.FormatValueCaller(
repoApi,
wb.dataTypeStore
),
userLanguages[0]
);
const htmlDataValueEntityIdFormatter = formatterFactory.getFormatter( null, null, 'text/html' );
const parserStore = wb.parsers.getStore( repoApi );
const entityIdParser = new ( parserStore.getParser( wb.datamodel.EntityId.TYPE ) )( { lang: userLanguages[ 0 ] } );
return new wb.entityIdFormatter.CachingEntityIdHtmlFormatter(
new wb.entityIdFormatter.DataValueBasedEntityIdHtmlFormatter(entityIdParser, htmlDataValueEntityIdFormatter)
);
})(),
dedup(array, hash_fn=null, overwrite=false) {
const map = new Map();
for (let x of array) {
const hash = hash_fn ? hash_fn(x) : x;
if (map.has(hash) && overwrite) map.set(hash, x);
else map.set(hash, x);
}
return map.values();
},
mapValues(obj, fn) {
const result = {};
for (let key in obj) {
result[key] = fn(obj[key]);
}
return result;
},
renderValue($container, propertyId, data) {
/**
* @return {string[]} An ordered list of languages the user wants to use, the first being her
* preferred language, and thus the UI language (currently wgUserLanguage).
*/
function getUserLanguages() {
var userLanguages = mw.config.get( 'wbUserSpecifiedLanguages' ),
isUlsDefined = mw.uls && $.uls && $.uls.data,
languages;
if ( !userLanguages.length && isUlsDefined ) {
languages = mw.uls.getFrequentLanguageList().slice( 1, 4 );
} else {
languages = userLanguages.slice();
languages.splice( $.inArray( mw.config.get( 'wgUserLanguage' ), userLanguages ), 1 );
}
languages.unshift( mw.config.get( 'wgUserLanguage' ) );
return languages;
}
/**
* @param {wikibase.api.RepoApi} repoApi
* @param {string} languageCode The language code of the ui language
* @return {wikibase.store.CachingEntityStore}
*/
function buildEntityStore( repoApi, languageCode ) {
return new wb.store.CachingEntityStore(
new wb.store.ApiEntityStore(
repoApi,
new wb.serialization.EntityDeserializer(),
[ languageCode ]
)
);
}
var currentRevision, revisionStore, entityChangersFactory,
viewFactoryArguments, ViewFactoryFactory, viewFactory, entityView,
repoConfig = mw.config.get( 'wbRepo' ),
repoApiUrl = repoConfig.url + repoConfig.scriptPath + '/api.php',
mwApi = wb.api.getLocationAgnosticMwApi( repoApiUrl ),
repoApi = new wb.api.RepoApi( mwApi ),
userLanguages = getUserLanguages(),
entityStore = buildEntityStore( repoApi, userLanguages[ 0 ] ),
contentLanguages = new wikibase.WikibaseContentLanguages(),
formatterFactory = new wb.formatters.ApiValueFormatterFactory(
new wb.api.FormatValueCaller(
repoApi,
wb.dataTypeStore
),
userLanguages[ 0 ]
);
var vvb = new wb.ValueViewBuilder (
wb.experts.getStore(wb.dataTypeStore), // expertStore,
formatterFactory,
wb.parsers.getStore( repoApi ), //parserStore,
userLanguages[ 0 ],
null, // messageProvider,
userLanguages,
null, // vocabularyLookupApiUrl,
null // commonsApiUrl
)
// var data = dataValues.MonolingualTextValue.newFromJSON({text: "FOO", language: "en"});
// vvb.initValueView($el, wb.dataTypeStore.getDataType('monolingualtext'), data, 'P1705').value(data);
// var typedData = dataValues.newDataValue(type, data);
const type = wb.dataTypeStore.getDataType(data.getType());
vvb.initValueView($container, type, data, propertyId).value(data);
}
};
tableView.selected = function($statementGroupView) {
mw.util.addCSS(tableView.css);
const qualsAsCols = true;
$statementGroupView.addClass(tableView.sgvClass);
const $lv = $statementGroupView.find('.wikibase-statementlistview-listview');
$lv.prepend(tableView.templates.DEFAULT_TABLE_HEADERS);
if (qualsAsCols) {
$statementGroupView.addClass('dc-quals-as-cols');
// Get a unique list of all the qualifiers
const $props = $statementGroupView.find(".wikibase-statementview-qualifiers .wikibase-snakview-property a");
const uniqProps = tableView.utils.dedup($props, a => a.title);
// create a column for each qualifier
const propTitles = [];
for (let propEl of uniqProps) {
propTitles.push(propEl.title);
const colIndex = createQualifierColumn($statementGroupView, $(propEl));
}
// copy each qualifier statement to the column
// iterate through statements
const statementViewEls = $statementGroupView.find('.wikibase-statementview');
for (let i = 0; i < statementViewEls.length; i++) {
const $sv = $(statementViewEls[i]);
const props = new Map(propTitles.map(t => [t, []]));
// iterate through qualifiers
let lastPropTitle = null;
for (let snakViewEl of $sv.find('.wikibase-statementview-qualifiers .wikibase-snakview')) {
const $snakView = $(snakViewEl);
let $prop = $snakView.find('.wikibase-snakview-property');
let a = $prop.find('a');
let propTitle = a.length ? a.attr('title') : lastPropTitle;
props.get(propTitle).push($snakView.find('.wikibase-snakview-value-container')[0]);
lastPropTitle = propTitle;
}
// add the cells
for (let j = 0; j < propTitles.length; j++) {
const propTitle = propTitles[j];
const $cell = $(`<div class="dc-extra-cell dc-qual-vals-cell" style="grid-column:${j+2}; grid-row: ${i+2}"></div>`);
const $values = props.get(propTitle).map($);
if (!$values.length) {
$cell.html('-');
}
for (let $val of $values) {
$cell.append($val.clone(false, true));
}
$cell.insertAfter($sv);
}
}
}
// Let's display references as a column too
const refsColIndex = createReferencesHeader($statementGroupView);
// copy refs out
const statementViewEls = $statementGroupView.find('.wikibase-statementview');
for (let i = 0; i < statementViewEls.length; i++) {
const $sv = $(statementViewEls[i]);
const $refs = $sv.find('.wikibase-statementview-references-container');
const $cell = $(`<div class="dc-extra-cell dc-ref-cell" style="grid-column:${refsColIndex}; grid-row: ${i+2}"></div>`);
const $refsCopy = $($refs[0].cloneNode(false));
$refsCopy.append($refs.find('.wikibase-statementview-references-heading'));
$cell.append($refsCopy);
$cell.insertAfter($sv);
}
const repoConfig = mw.config.get('wbRepo');
const repoApiUrl = repoConfig.url + repoConfig.scriptPath + '/api.php'
$lv.find('.dc-add-header input')
.entityselector({url: repoApiUrl, type:'property'})
.on('entityselectorselected', async (ev, pid) => {
const $addHeader = $statementGroupView.find('.dc-add-header');
// insert column data
await insertColumn(ev, pid, $addHeader);
// Stop adding
$addHeader.removeClass('adding');
$addHeader.addClass('pre-add');
$addHeader.find('input').val('');
});
$('.wikibase-statementgroupview').on('click', '.dc-add-header-plus', ev => {
// swap out the plus for the input dialog and a cancel
const $addHeader = $(ev.target).closest('.dc-add-header');
$addHeader.removeClass('pre-add');
$addHeader.addClass('adding');
$addHeader.find('input').focus();
});
$('.wikibase-statementgroupview').on('click', '.dc-add-header-x', ev => {
// Undo swap
const $addHeader = $(ev.target).closest('.dc-add-header');
$addHeader.removeClass('adding');
$addHeader.addClass('pre-add');
});
function createReferencesHeader($statementGroupView) {
const $lv = $statementGroupView.find('.wikibase-statementlistview-listview');
const colIndex = $statementGroupView.find('.dc-grid-header').length;
const $prev = $statementGroupView.find('.dc-grid-header').last();
const $el = $(`<div class="dc-grid-header dc-refs-col" style="grid-column:${colIndex}">References</div>`).insertAfter($prev);
return colIndex;
}
function createQualifierColumn($statementGroupView, $prop) {
const $lv = $statementGroupView.find('.wikibase-statementlistview-listview');
// Create header
const colIndex = $statementGroupView.find('.dc-grid-header').length;
const $prev = $statementGroupView.find('.dc-grid-header:first-child,.dc-grid-header.dc-qualifier-header').last();
const $el = $(`<div class="dc-grid-header dc-qual-col" style="grid-column:${colIndex}"></div>`).insertAfter($prev);
$el.append($prop.clone(false, true));
return colIndex;
}
async function insertColumn(ev, colPid, $addHeader) {
const {entityFormatter, renderValue, mapValues} = tableView.utils;
const colIndex = $(ev.target)
.closest('.wikibase-statementgroupview')
.find('.dc-grid-header')
.length;
// insert column header
$(`<div class="dc-grid-header" style="grid-column:${colIndex}">${await entityFormatter.format(colPid)}</div>`).insertBefore($addHeader);
const pid = $(ev.target)
.closest('.wikibase-statementgroupview')
.data('property-id');
const sparql = tableView.templates.NEW_COL_SPARQL(mw.config.get('wbEntityId'), pid, colPid);
const resp = await fetch('https://query.wikidata.org/sparql?format=json&query=' + encodeURIComponent(sparql))
.then(r => r.json());
const cleanedBindings = resp.results.bindings
.map(b => mapValues(b, v => v.value))
.map(b => {
if (!b.type) return [b.x, null, null];
switch (b.type) {
case "time":
return [b.x, b.type, {
time: b.timeTime,
precision: parseFloat(b.timePrecision),
timezone: parseFloat(b.timeTimezone),
calendarmodel: b.timeCalendarModel,
before: 0,
after: 0
}];
case "quantity":
return [b.x, b.type, {
amount: parseFloat(b.quantityAmount),
unit: b.quantityUnit.endsWith("Q199") ? "1" : b.quantityUnit
}];
case "monolingualtext":
return [b.x, b.type, {
text: b.monolingualtextText,
language: b.monolingualtextLanguage
}];
case "item":
return [b.x, "wikibase-entityid", {
"entity-type": "item",
"numeric-id": parseFloat(b.itemId.match(/\d+$/)[0]),
id: b.itemId.match(/Q\d+$/)[0]
}];
case "string":
return [b.x, b.type, b.string];
default:
throw new Error("NOT IMPLEMENTED");
}
});
// Group results by key
const resultsByKey = new Map();
for (let [key, type, datavalue] of cleanedBindings) {
key = key.split('/')[4];
if (!datavalue) continue;
const typedValue = dataValues.newDataValue(type, datavalue);
if (resultsByKey.has(key)) resultsByKey.get(key).push(typedValue);
else resultsByKey.set(key, [typedValue]);
}
// try to render!
const statements = JSON.parse(mw.config.get('wbEntity')).claims[pid];
const $lv = $(ev.target).closest('.wikibase-statementgroupview').find('.wikibase-statementlistview-listview');
for (let i = 0; i < statements.length; i++) {
const s = statements[i];
const qid = s.mainsnak.datavalue.value.id;
if (resultsByKey.has(qid)) {
const $cell = $(`<div class="dc-extra-cell" style="grid-column:${colIndex}; grid-row: ${i+2}"></div>`);
$lv.append($cell);
const renderedItems = resultsByKey.get(qid)
.map(data => {
const $div = $('<div/>');
$div.appendTo($cell);
renderValue($div, colPid, data);
});
}
else {
$lv.append(`<div class="dc-extra-cell" style="grid-column:${colIndex}; grid-row: ${i+2}">-</div>`);
}
}
}
};
}
const HTML_TEMPLATES = {
LayoutOptions() {
return `<div class="dc-statementgroupview-layout-options">
[ <a href="javascript:;">List</a> | <a href="javascript:;">Table</a> ]
</div>`;
}
};
$('.wikibase-statementgroupview-property').append($(HTML_TEMPLATES.LayoutOptions()));
$('.dc-statementgroupview-layout-options a').click((ev) => {
const $el = $(ev.target);
const $statementGroupView = $el.closest('.wikibase-statementgroupview');
const viewsByName = {
"Table": tableView,
"List": listView
}
const selectedViewName = $el.text();
const view = viewsByName[selectedViewName];
if (view) view.selected($statementGroupView);
});
console.log("wdTableView Initialized.")
})();