modules/ui/fields/wikidata.js (256 lines of code) (raw):
import { dispatch as d3_dispatch } from 'd3-dispatch';
import {
select as d3_select,
event as d3_event
} from 'd3-selection';
import { uiCombobox } from '../combobox';
import { actionChangeTags } from '../../actions/change_tags';
import { services } from '../../services/index';
import { svgIcon } from '../../svg/icon';
import {
utilGetSetValue,
utilNoAuto,
utilRebind
} from '../../util';
import { t } from '../../util/locale';
export function uiFieldWikidata(field, context) {
var wikidata = services.wikidata;
var dispatch = d3_dispatch('change');
var searchInput = d3_select(null);
var _qid = null;
var _wikidataEntity = null;
var _wikiURL = '';
var _entity;
var _wikipediaKey = field.keys && field.keys.find(function(key) {
return key.includes('wikipedia');
}),
_hintKey = field.key === 'wikidata' ? 'name' : field.key.split(':')[0];
var combobox = uiCombobox(context, 'combo-' + field.safeid)
.caseSensitive(true)
.minItems(1);
function wiki(selection) {
var wrap = selection.selectAll('.form-field-input-wrap')
.data([0]);
wrap = wrap.enter()
.append('div')
.attr('class', 'form-field-input-wrap form-field-input-' + field.type)
.merge(wrap);
var list = wrap.selectAll('ul')
.data([0]);
list = list.enter()
.append('ul')
.attr('class', 'rows')
.merge(list);
var searchRow = list.selectAll('li.wikidata-search')
.data([0]);
var searchRowEnter = searchRow.enter()
.append('li')
.attr('class', 'wikidata-search');
searchInput = searchRowEnter
.append('input')
.attr('type', 'text')
.style('flex', '1')
.call(utilNoAuto);
searchInput
.on('focus', function() {
var node = d3_select(this).node();
node.setSelectionRange(0, node.value.length);
})
.on('blur', function() {
setLabelForEntity();
})
.call(combobox.fetcher(fetchWikidataItems));
combobox.on('accept', function(d) {
_qid = d.id;
change();
}).on('cancel', function() {
setLabelForEntity();
});
searchRowEnter
.append('button')
.attr('class', 'form-field-button wiki-link')
.attr('title', t('icons.open_wikidata'))
.attr('tabindex', -1)
.call(svgIcon('#iD-icon-out-link'))
.on('click', function() {
d3_event.preventDefault();
if (_wikiURL) window.open(_wikiURL, '_blank');
});
var wikidataProperties = ['description', 'identifier'];
var items = list.selectAll('li.labeled-input')
.data(wikidataProperties);
// Enter
var enter = items.enter()
.append('li')
.attr('class', function(d) { return 'labeled-input preset-wikidata-' + d; });
enter
.append('span')
.attr('class', 'label')
.attr('for', function(d) { return 'preset-input-wikidata-' + d; })
.text(function(d) { return t('wikidata.' + d); });
enter
.append('input')
.attr('type', 'text')
.attr('id', function(d) { return 'preset-input-wikidata-' + d; })
.call(utilNoAuto)
.classed('disabled', 'true')
.attr('readonly', 'true');
enter
.append('button')
.attr('class', 'form-field-button')
.attr('title', t('icons.copy'))
.attr('tabindex', -1)
.call(svgIcon('#iD-operation-copy'))
.on('click', function() {
d3_event.preventDefault();
d3_select(this.parentNode)
.select('input')
.node()
.select();
document.execCommand('copy');
});
}
function fetchWikidataItems(q, callback) {
if (!q && _entity) {
q = (_hintKey && context.entity(_entity.id).tags[_hintKey]) || '';
}
wikidata.itemsForSearchQuery(q, function(err, data) {
if (err) return;
for (var i in data) {
data[i].value = data[i].label + ' (' + data[i].id + ')';
data[i].title = data[i].description;
}
if (callback) callback(data);
});
}
function change() {
var syncTags = {};
syncTags[field.key] = _qid;
dispatch.call('change', this, syncTags);
// attempt asynchronous update of wikidata tag..
var initGraph = context.graph();
var initEntityID = _entity.id;
wikidata.entityByQID(_qid, function(err, entity) {
if (err) return;
// If graph has changed, we can't apply this update.
if (context.graph() !== initGraph) return;
if (!entity.sitelinks) return;
var langs = wikidata.languagesToQuery();
// use the label and description languages as fallbacks
['labels', 'descriptions'].forEach(function(key) {
if (!entity[key]) return;
var valueLangs = Object.keys(entity[key]);
if (valueLangs.length === 0) return;
var valueLang = valueLangs[0];
if (langs.indexOf(valueLang) === -1) {
langs.push(valueLang);
}
});
var currTags = Object.assign({}, context.entity(initEntityID).tags); // shallow copy
if (_wikipediaKey) {
var foundPreferred;
for (var i in langs) {
var lang = langs[i];
var siteID = lang.replace('-', '_') + 'wiki';
if (entity.sitelinks[siteID]) {
foundPreferred = true;
currTags[_wikipediaKey] = lang + ':' + entity.sitelinks[siteID].title;
// use the first match
break;
}
}
if (!foundPreferred) {
// No wikipedia sites available in the user's language or the fallback languages,
// default to any wikipedia sitelink
var wikiSiteKeys = Object.keys(entity.sitelinks).filter(function(site) {
return site.endsWith('wiki');
});
if (wikiSiteKeys.length === 0) {
// if no wikipedia pages are linked to this wikidata entity, delete that tag
if (currTags[_wikipediaKey]) {
delete currTags[_wikipediaKey];
}
} else {
var wikiLang = wikiSiteKeys[0].slice(0, -4).replace('_', '-');
var wikiTitle = entity.sitelinks[wikiSiteKeys[0]].title;
currTags[_wikipediaKey] = wikiLang + ':' + wikiTitle;
}
}
}
// Coalesce the update of wikidata tag into the previous tag change
context.overwrite(
actionChangeTags(initEntityID, currTags),
context.history().undoAnnotation()
);
// do not dispatch.call('change') here, because entity_editor
// changeTags() is not intended to be called asynchronously
});
}
function setLabelForEntity() {
var label = '';
if (_wikidataEntity) {
label = entityPropertyForDisplay(_wikidataEntity, 'labels');
if (label.length === 0) {
label = _wikidataEntity.id.toString();
}
}
utilGetSetValue(d3_select('li.wikidata-search input'), label);
}
wiki.tags = function(tags) {
_qid = tags[field.key] || '';
if (!/^Q[0-9]*$/.test(_qid)) { // not a proper QID
unrecognized();
return;
}
// QID value in correct format
_wikiURL = 'https://wikidata.org/wiki/' + _qid;
wikidata.entityByQID(_qid, function(err, entity) {
if (err) {
unrecognized();
return;
}
_wikidataEntity = entity;
setLabelForEntity();
var description = entityPropertyForDisplay(entity, 'descriptions');
d3_select('.form-field-wikidata button.wiki-link')
.classed('disabled', false);
d3_select('.preset-wikidata-description')
.style('display', function(){
return description.length > 0 ? 'flex' : 'none';
})
.select('input')
.attr('value', description);
d3_select('.preset-wikidata-identifier')
.style('display', function(){
return entity.id ? 'flex' : 'none';
})
.select('input')
.attr('value', entity.id);
});
// not a proper QID
function unrecognized() {
_wikidataEntity = null;
setLabelForEntity();
d3_select('.preset-wikidata-description')
.style('display', 'none');
d3_select('.preset-wikidata-identifier')
.style('display', 'none');
d3_select('.form-field-wikidata button.wiki-link')
.classed('disabled', true);
if (_qid && _qid !== '') {
_wikiURL = 'https://wikidata.org/wiki/Special:Search?search=' + _qid;
} else {
_wikiURL = '';
}
}
};
function entityPropertyForDisplay(wikidataEntity, propKey) {
if (!wikidataEntity[propKey]) return '';
var propObj = wikidataEntity[propKey];
var langKeys = Object.keys(propObj);
if (langKeys.length === 0) return '';
// sorted by priority, since we want to show the user's language first if possible
var langs = wikidata.languagesToQuery();
for (var i in langs) {
var lang = langs[i];
var valueObj = propObj[lang];
if (valueObj && valueObj.value && valueObj.value.length > 0) return valueObj.value;
}
// default to any available value
return propObj[langKeys[0]].value;
}
wiki.entity = function(val) {
if (!arguments.length) return _entity;
_entity = val;
return wiki;
};
wiki.focus = function() {
searchInput.node().focus();
};
return utilRebind(wiki, dispatch, 'on');
}