modules/ui/fields/localized.js (385 lines of code) (raw):
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
import { utilArrayUniq, utilUniqueString } from '@id-sdk/util';
import * as countryCoder from '@ideditor/country-coder';
import { presetManager } from '../../presets';
import { fileFetcher } from '../../core/file_fetcher';
import { t, localizer } from '../../core/localizer';
import { services } from '../../services';
import { svgIcon } from '../../svg';
import { uiTooltip } from '../tooltip';
import { uiCombobox } from '../combobox';
import { utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent } from '../../util';
var _languagesArray = [];
export function uiFieldLocalized(field, context) {
var dispatch = d3_dispatch('change', 'input');
var wikipedia = services.wikipedia;
var input = d3_select(null);
var localizedInputs = d3_select(null);
var _countryCode;
var _tags;
// A concern here in switching to async data means that _languagesArray will not
// be available the first time through, so things like the fetchers and
// the language() function will not work immediately.
fileFetcher.get('languages')
.then(loadLanguagesArray)
.catch(function() { /* ignore */ });
var _territoryLanguages = {};
fileFetcher.get('territory_languages')
.then(function(d) { _territoryLanguages = d; })
.catch(function() { /* ignore */ });
// reuse these combos
var langCombo = uiCombobox(context, 'localized-lang')
.fetcher(fetchLanguages)
.minItems(0);
var _selection = d3_select(null);
var _multilingual = [];
var _buttonTip = uiTooltip()
.title(t.html('translate.translate'))
.placement('left');
var _wikiTitles;
var _entityIDs = [];
function loadLanguagesArray(dataLanguages) {
if (_languagesArray.length !== 0) return;
// some conversion is needed to ensure correct OSM tags are used
var replacements = {
sr: 'sr-Cyrl', // in OSM, `sr` implies Cyrillic
'sr-Cyrl': false // `sr-Cyrl` isn't used in OSM
};
for (var code in dataLanguages) {
if (replacements[code] === false) continue;
var metaCode = code;
if (replacements[code]) metaCode = replacements[code];
_languagesArray.push({
localName: localizer.languageName(metaCode, { localOnly: true }),
nativeName: dataLanguages[metaCode].nativeName,
code: code,
label: localizer.languageName(metaCode)
});
}
}
function calcLocked() {
// Protect name field for suggestion presets that don't display a brand/operator field
var isLocked = (field.id === 'name') &&
_entityIDs.length &&
_entityIDs.some(function(entityID) {
var entity = context.graph().hasEntity(entityID);
if (!entity) return false;
// Features linked to Wikidata are likely important and should be protected
if (entity.tags.wikidata) return true;
// Assume the name has already been confirmed if its source has been researched
if (entity.tags['name:etymology:wikidata']) return true;
// Lock the `name` if this is a suggestion preset that assigns the name,
// and the preset does not display a `brand` or `operator` field.
// (For presets like hotels, car dealerships, post offices, the `name` should remain editable)
// see also similar logic in `outdated_tags.js`
var preset = presetManager.match(entity, context.graph());
if (preset) {
var isSuggestion = preset.suggestion;
var fields = preset.fields();
var showsBrandField = fields.some(function(d) { return d.id === 'brand'; });
var showsOperatorField = fields.some(function(d) { return d.id === 'operator'; });
var setsName = preset.addTags.name;
var setsBrandWikidata = preset.addTags['brand:wikidata'];
var setsOperatorWikidata = preset.addTags['operator:wikidata'];
return (isSuggestion && setsName && (
(setsBrandWikidata && !showsBrandField) ||
(setsOperatorWikidata && !showsOperatorField)
));
}
return false;
});
field.locked(isLocked);
}
// update _multilingual, maintaining the existing order
function calcMultilingual(tags) {
var existingLangsOrdered = _multilingual.map(function(item) {
return item.lang;
});
var existingLangs = new Set(existingLangsOrdered.filter(Boolean));
for (var k in tags) {
var m = k.match(/^(.*):(.*)$/);
if (m && m[1] === field.key && m[2]) {
var item = { lang: m[2], value: tags[k] };
if (existingLangs.has(item.lang)) {
// update the value
_multilingual[existingLangsOrdered.indexOf(item.lang)].value = item.value;
existingLangs.delete(item.lang);
} else {
_multilingual.push(item);
}
}
}
// Don't remove items based on deleted tags, since this makes the UI
// disappear unexpectedly when clearing values - #8164
_multilingual.forEach(function(item) {
if (item.lang && existingLangs.has(item.lang)) {
item.value = '';
}
});
}
function localized(selection) {
_selection = selection;
calcLocked();
var isLocked = field.locked();
var wrap = selection.selectAll('.form-field-input-wrap')
.data([0]);
// enter/update
wrap = wrap.enter()
.append('div')
.attr('class', 'form-field-input-wrap form-field-input-' + field.type)
.merge(wrap);
input = wrap.selectAll('.localized-main')
.data([0]);
// enter/update
input = input.enter()
.append('input')
.attr('type', 'text')
.attr('id', field.domId)
.attr('class', 'localized-main')
.call(utilNoAuto)
.merge(input);
input
.classed('disabled', !!isLocked)
.attr('readonly', isLocked || null)
.on('input', change(true))
.on('blur', change())
.on('change', change());
var translateButton = wrap.selectAll('.localized-add')
.data([0]);
translateButton = translateButton.enter()
.append('button')
.attr('class', 'localized-add form-field-button')
.call(svgIcon('#iD-icon-plus'))
.merge(translateButton);
translateButton
.classed('disabled', !!isLocked)
.call(isLocked ? _buttonTip.destroy : _buttonTip)
.on('click', addNew);
if (_tags && !_multilingual.length) {
calcMultilingual(_tags);
}
localizedInputs = selection.selectAll('.localized-multilingual')
.data([0]);
localizedInputs = localizedInputs.enter()
.append('div')
.attr('class', 'localized-multilingual')
.merge(localizedInputs);
localizedInputs
.call(renderMultilingual);
localizedInputs.selectAll('button, input')
.classed('disabled', !!isLocked)
.attr('readonly', isLocked || null);
function addNew(d3_event) {
d3_event.preventDefault();
if (field.locked()) return;
var defaultLang = localizer.languageCode().toLowerCase();
var langExists = _multilingual.find(function(datum) { return datum.lang === defaultLang; });
var isLangEn = defaultLang.indexOf('en') > -1;
if (isLangEn || langExists) {
defaultLang = '';
langExists = _multilingual.find(function(datum) { return datum.lang === defaultLang; });
}
if (!langExists) {
// prepend the value so it appears at the top
_multilingual.unshift({ lang: defaultLang, value: '' });
localizedInputs
.call(renderMultilingual);
}
}
function change(onInput) {
return function(d3_event) {
if (field.locked()) {
d3_event.preventDefault();
return;
}
var val = utilGetSetValue(d3_select(this));
if (!onInput) val = context.cleanTagValue(val);
// don't override multiple values with blank string
if (!val && Array.isArray(_tags[field.key])) return;
var t = {};
t[field.key] = val || undefined;
dispatch.call('change', this, t, onInput);
};
}
}
function key(lang) {
return field.key + ':' + lang;
}
function changeLang(d3_event, d) {
var tags = {};
// make sure unrecognized suffixes are lowercase - #7156
var lang = utilGetSetValue(d3_select(this)).toLowerCase();
var language = _languagesArray.find(function(d) {
return d.label.toLowerCase() === lang ||
(d.localName && d.localName.toLowerCase() === lang) ||
(d.nativeName && d.nativeName.toLowerCase() === lang);
});
if (language) lang = language.code;
if (d.lang && d.lang !== lang) {
tags[key(d.lang)] = undefined;
}
var newKey = lang && context.cleanTagKey(key(lang));
var value = utilGetSetValue(d3_select(this.parentNode).selectAll('.localized-value'));
if (newKey && value) {
tags[newKey] = value;
} else if (newKey && _wikiTitles && _wikiTitles[d.lang]) {
tags[newKey] = _wikiTitles[d.lang];
}
d.lang = lang;
dispatch.call('change', this, tags);
}
function changeValue(d3_event, d) {
if (!d.lang) return;
var value = context.cleanTagValue(utilGetSetValue(d3_select(this))) || undefined;
// don't override multiple values with blank string
if (!value && Array.isArray(d.value)) return;
var t = {};
t[key(d.lang)] = value;
d.value = value;
dispatch.call('change', this, t);
}
function fetchLanguages(value, cb) {
var v = value.toLowerCase();
// show the user's language first
var langCodes = [localizer.localeCode(), localizer.languageCode()];
if (_countryCode && _territoryLanguages[_countryCode]) {
langCodes = langCodes.concat(_territoryLanguages[_countryCode]);
}
var langItems = [];
langCodes.forEach(function(code) {
var langItem = _languagesArray.find(function(item) {
return item.code === code;
});
if (langItem) langItems.push(langItem);
});
langItems = utilArrayUniq(langItems.concat(_languagesArray));
cb(langItems.filter(function(d) {
return d.label.toLowerCase().indexOf(v) >= 0 ||
(d.localName && d.localName.toLowerCase().indexOf(v) >= 0) ||
(d.nativeName && d.nativeName.toLowerCase().indexOf(v) >= 0) ||
d.code.toLowerCase().indexOf(v) >= 0;
}).map(function(d) {
return { value: d.label };
}));
}
function renderMultilingual(selection) {
var entries = selection.selectAll('div.entry')
.data(_multilingual, function(d) { return d.lang; });
entries.exit()
.style('top', '0')
.style('max-height', '240px')
.transition()
.duration(200)
.style('opacity', '0')
.style('max-height', '0px')
.remove();
var entriesEnter = entries.enter()
.append('div')
.attr('class', 'entry')
.each(function(_, index) {
var wrap = d3_select(this);
var domId = utilUniqueString(index);
var label = wrap
.append('label')
.attr('class', 'field-label')
.attr('for', domId);
var text = label
.append('span')
.attr('class', 'label-text');
text
.append('span')
.attr('class', 'label-textvalue')
.html(t.html('translate.localized_translation_label'));
text
.append('span')
.attr('class', 'label-textannotation');
label
.append('button')
.attr('class', 'remove-icon-multilingual')
.on('click', function(d3_event, d) {
if (field.locked()) return;
d3_event.preventDefault();
// remove the UI item manually
_multilingual.splice(_multilingual.indexOf(d), 1);
var langKey = d.lang && key(d.lang);
if (langKey && langKey in _tags) {
delete _tags[langKey];
// remove from entity tags
var t = {};
t[langKey] = undefined;
dispatch.call('change', this, t);
return;
}
renderMultilingual(selection);
})
.call(svgIcon('#iD-operation-delete'));
wrap
.append('input')
.attr('class', 'localized-lang')
.attr('id', domId)
.attr('type', 'text')
.attr('placeholder', t('translate.localized_translation_language'))
.on('blur', changeLang)
.on('change', changeLang)
.call(langCombo);
wrap
.append('input')
.attr('type', 'text')
.attr('class', 'localized-value')
.on('blur', changeValue)
.on('change', changeValue);
});
entriesEnter
.style('margin-top', '0px')
.style('max-height', '0px')
.style('opacity', '0')
.transition()
.duration(200)
.style('margin-top', '10px')
.style('max-height', '240px')
.style('opacity', '1')
.on('end', function() {
d3_select(this)
.style('max-height', '')
.style('overflow', 'visible');
});
entries = entries.merge(entriesEnter);
entries.order();
// allow removing the entry UIs even if there isn't a tag to remove
entries.classed('present', true);
utilGetSetValue(entries.select('.localized-lang'), function(d) {
var langItem = _languagesArray.find(function(item) {
return item.code === d.lang;
});
if (langItem) return langItem.label;
return d.lang;
});
utilGetSetValue(entries.select('.localized-value'), function(d) {
return typeof d.value === 'string' ? d.value : '';
})
.attr('title', function(d) {
return Array.isArray(d.value) ? d.value.filter(Boolean).join('\n') : null;
})
.attr('placeholder', function(d) {
return Array.isArray(d.value) ? t('inspector.multiple_values') : t('translate.localized_translation_name');
})
.classed('mixed', function(d) {
return Array.isArray(d.value);
});
}
localized.tags = function(tags) {
_tags = tags;
// Fetch translations from wikipedia
if (typeof tags.wikipedia === 'string' && !_wikiTitles) {
_wikiTitles = {};
var wm = tags.wikipedia.match(/([^:]+):(.+)/);
if (wm && wm[0] && wm[1]) {
wikipedia.translations(wm[1], wm[2], function(err, d) {
if (err || !d) return;
_wikiTitles = d;
});
}
}
var isMixed = Array.isArray(tags[field.key]);
utilGetSetValue(input, typeof tags[field.key] === 'string' ? tags[field.key] : '')
.attr('title', isMixed ? tags[field.key].filter(Boolean).join('\n') : undefined)
.attr('placeholder', isMixed ? t('inspector.multiple_values') : field.placeholder())
.classed('mixed', isMixed);
calcMultilingual(tags);
_selection
.call(localized);
};
localized.focus = function() {
input.node().focus();
};
localized.entityIDs = function(val) {
if (!arguments.length) return _entityIDs;
_entityIDs = val;
_multilingual = [];
loadCountryCode();
return localized;
};
function loadCountryCode() {
var extent = combinedEntityExtent();
var countryCode = extent && countryCoder.iso1A2Code(extent.center());
_countryCode = countryCode && countryCode.toLowerCase();
}
function combinedEntityExtent() {
return _entityIDs && _entityIDs.length && utilTotalExtent(_entityIDs, context.graph());
}
return utilRebind(localized, dispatch, 'on');
}