enketo-core
Version:
Extensible Enketo form engine
207 lines (182 loc) • 6.55 kB
JavaScript
/**
* Form languages module.
*
* @module language
*/
import { getSiblingElement } from './dom-utils';
import events from './event';
/**
* @typedef {import('./form').Form} Form
*/
export default {
/**
* @type {Form}
*/
// @ts-expect-error - this will be populated during form init, but assigning
// its type here improves intellisense.
form: null,
/**
* @param {string} overrideLang - override language IANA subtag
*/
init(overrideLang) {
if (!this.form) {
throw new Error(
'Language module not correctly instantiated with form property.'
);
}
const root =
this.form.view.html.closest('body') ||
this.form.view.html.parentNode;
if (!root) {
return;
}
const langSelector = root.querySelector('.form-language-selector');
const formLanguages =
this.form.view.html.querySelector('#form-languages');
if (!formLanguages) {
return;
}
this.languages = [...formLanguages.querySelectorAll('option')].map(
(option) => option.value
);
if (langSelector) {
langSelector.append(formLanguages);
if (this.languages.length > 1) {
langSelector.classList.remove('hide');
}
}
this.formLanguages = root.querySelector('#form-languages');
this.defaultLanguage =
this.formLanguages.dataset.defaultLang || undefined;
if (
overrideLang &&
this.languages.includes(overrideLang) &&
this.languages.length > 1
) {
this._currentLang = overrideLang;
this.setFormUi(this._currentLang);
} else {
this._currentLang = this.defaultLanguage || this.languages[0] || '';
}
const langOption = this.formLanguages.querySelector(
`[value="${this._currentLang}"]`
);
const currentDirectionality =
(langOption && langOption.dataset.dir) || 'ltr';
this.formLanguages.value = this._currentLang;
this.form.view.html.setAttribute('dir', currentDirectionality);
if (this.languages.length < 2) {
return;
}
this.formLanguages.addEventListener(events.Change().type, (event) => {
event.preventDefault();
this._currentLang = event.target.value;
this.setFormUi(this._currentLang);
});
this.form.view.html.addEventListener(events.AddRepeat().type, (event) =>
this.setFormUi(this._currentLang, event.target)
);
},
/**
* @type {string}
*/
get currentLanguage() {
return this._currentLang;
},
/**
* @type {string}
*/
get currentLangDesc() {
const langOption = this.formLanguages.querySelector(
`[value="${this._currentLang}"]`
);
return langOption ? langOption.textContent : null;
},
/**
* @type {Array}
*/
get languagesUsed() {
return this.languages || [];
},
/**
* @param {string} lang
* @param {HTMLElement} group
*/
setFormUi(lang, group = this.form.view.html) {
if (group.dataset.currentLang === lang) {
return;
}
group.dataset.currentLang = lang;
if (group === this.form.view.html) {
this.form.collections.repeats
.getElements()
.forEach((repeatInstance) => {
repeatInstance.dataset.lang = lang;
});
}
const dir =
this.formLanguages.querySelector(`[value="${lang}"]`).dataset.dir ||
'ltr';
const translations = [...group.querySelectorAll('[lang]')];
this.form.view.html.setAttribute('dir', dir);
translations.forEach((el) => el.classList.remove('active'));
translations
.filter(
(el) =>
el.matches(`[lang="${lang}"], [lang=""]`) &&
(!el.classList.contains('or-form-short') ||
(el.classList.contains('or-form-short') &&
!getSiblingElement(el, '.or-form-long')))
)
.forEach((el) => el.classList.add('active'));
// For use in locale-sensitive XPath functions.
// Don't even check whether it's a proper subtag or not. It will revert to client locale if it is not recognized.
window.enketoFormLocale = lang;
// TODO: can these be restricted to `group`?
this.form.view.html
.querySelectorAll('select, datalist')
.forEach((el) => this.setSelect(el));
this.form.view.html.dispatchEvent(events.ChangeLanguage());
},
/**
* swap language of <select> and <datalist> <option>s
*
* @param {Element} select - select or datalist HTML element
*/
setSelect(select) {
const type = select.nodeName.toLowerCase();
const question = select.closest('.question, .or-repeat-info');
const translations = question
? question.querySelector('.or-option-translations')
: null;
if (!translations) {
return;
}
[...select.children]
.filter(
(el) =>
el.matches('option') &&
!el.matches('[value=""], [data-value=""]')
)
.forEach((option) => {
const curLabel =
type === 'datalist' ? option.value : option.textContent;
// Datalist will not have initialized when init function is called upon form load, so it is option.value until it has initialized. That is not great.
const value =
type === 'datalist'
? option.dataset.value || option.value
: option.value;
const translatedOption = translations.querySelector(
`.active[data-option-value="${CSS.escape(value)}"]`
);
if (translatedOption) {
let newLabel = curLabel;
if (translatedOption && translatedOption.textContent) {
newLabel = translatedOption.textContent;
}
option.value = value;
option.textContent = newLabel;
}
});
},
};