User:Nardog/IPAInput-core.js
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
(function ipaInputCore() {
mw.loader.addStyleTag('.ipainput-config{padding:0 1em 1em} .ipainput-input{position:sticky;top:0;left:0;opacity:0.8;font-size:200%;z-index:999} .ipainput-input > input{text-align:center} .ipainput .ipainput-input.oo-ui-indicatorElement > input{padding-right:56px} .ipainput .ipainput-input > .oo-ui-indicator-clear, .ipainput-symbol{cursor:pointer} .ipainput-undo, .ipainput-diaonly{font-size:50%;position:absolute;right:0;margin:0} .ipainput-diaonly{top:150%} .ipainput-status{text-align:center;font-size:120%;padding:1em 0 0.5em} .ipainput-status > a{font-weight:bold} .ipainput .mw-parser-output{margin:auto;width:max-content;max-width:100%;padding:0 0.5em;overflow:auto} .ipainput-symbol:hover{background-color:#fff} .ipainput-symbol:active{background-color:#c8ccd1} .ipainput-symbol:focus{outline:solid 2px #36c} .ipainput-symbol-disabled, .ipainput-symbol-disabled:hover, .ipainput-symbol-disabled:active, .ipainput-symbol-disabled:focus{cursor:auto;background-color:#c8ccd1 !important;outline:0} .ipainput-symbol-disabled, .ipainput-symbol-disabled a{color:#fff}');
let promise = mw.loader.using([
'jquery.textSelection', 'oojs-ui-windows', 'oojs-ui-widgets',
'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-editing-core',
'oojs-ui.styles.icons-editing-advanced'
]);
let langs = mw.storage.getObject('ipainput-cache');
if (!langs) {
mw.notify('Retrieving keys...', { autoHide: false, tag: 'ipainput' });
promise = $.when(promise, $.post('//en.wikipedia.org/api/rest_v1/transform/wikitext/to/html', {
wikitext: '{{#invoke:IPA/overview|keys}}',
body_only: true
}).then(response => {
langs = {
'(Full IPA chart)': [
['und', 'Undetermined'],
['', '(No linking)']
],
'English': [
['en', 'English']
]
};
let lastKey, lastLang;
$($.parseHTML(response)).find('td:last-child').each(function () {
let key, lang;
let prev = this.previousElementSibling;
if (prev) {
lang = prev.textContent;
lastLang = lang;
let prevPrev = prev.previousElementSibling;
if (prevPrev) {
key = prevPrev.textContent.slice(9);
lastKey = key;
} else {
key = lastKey;
}
} else {
key = lastKey;
lang = lastLang;
}
if (key === 'English') return;
if (!langs.hasOwnProperty(key)) langs[key] = [];
langs[key].push([this.textContent, lang]);
});
mw.requestIdleCallback(() => {
let notif = $('.mw-notification-tag-ipainput').data('mw-notification');
if (notif) notif.close();
mw.storage.setObject('ipainput-cache', langs, 604800);
});
}));
}
promise.then(() => {
function IpaInputDialog(config) {
IpaInputDialog.parent.call(this, config);
this.$element.addClass('ipainput');
}
OO.inheritClass(IpaInputDialog, OO.ui.ProcessDialog);
IpaInputDialog.static.name = 'ipaInputDialog';
IpaInputDialog.static.title = 'IPAInput';
IpaInputDialog.static.size = 'small';
IpaInputDialog.static.actions = [
{
modes: 'config',
flags: ['safe', 'close']
},
{
action: 'transcribe',
label: 'Transcribe',
modes: 'config',
flags: ['primary', 'progressive']
},
{
action: 'goBack',
modes: 'transcription',
flags: ['safe', 'back']
},
{
action: 'insert',
label: 'Insert',
modes: 'transcription',
flags: ['primary', 'progressive']
}
];
IpaInputDialog.prototype.initialize = function () {
IpaInputDialog.parent.prototype.initialize.apply(this, arguments);
this.keyDropdown = new OO.ui.DropdownWidget({
$overlay: this.$overlay,
menu: {
items: Object.keys(langs).map(k => (
new OO.ui.MenuOptionWidget({ label: k })
))
}
});
this.keyDropdown.getMenu().on('select', item => {
let options = langs[item.getLabel()].map(([code, lang]) => (
new OO.ui.MenuOptionWidget({
data: code,
label: code ? `${lang} (${code})` : lang
})
));
this.languageDropdown.getMenu().clearItems().addItems(options)
.selectItem(options[0]);
});
this.languageDropdown = new OO.ui.DropdownWidget({
$overlay: this.$overlay
});
this.noTemplateCheckbox = new OO.ui.CheckboxInputWidget()
.connect(this.languageDropdown, { change: 'setDisabled' });
this.rememberCheckbox = new OO.ui.CheckboxInputWidget();
this.form = new OO.ui.FormLayout({
items: [
new OO.ui.FieldLayout(this.keyDropdown, {
label: 'Key:',
align: 'top'
}),
new OO.ui.FieldLayout(this.languageDropdown, {
label: 'Language:',
align: 'top'
}),
new OO.ui.FieldLayout(this.noTemplateCheckbox, {
label: 'No template',
align: 'inline'
}),
new OO.ui.FieldLayout(this.rememberCheckbox, {
label: 'Remember these for next time',
align: 'inline'
})
],
content: [$('<input>').attr({ type: 'submit', hidden: '' })],
classes: ['ipainput-config']
}).connect(this, { submit: ['executeAction', 'transcribe'] });
this.$body.append(this.form.$element);
this.input = new OO.ui.TextInputWidget({
spellcheck: false,
classes: ['ipainput-input', 'IPA']
}).on('change', value => {
this.input.setIndicator(value ? 'clear' : null);
}).connect(this, { enter: ['executeAction', 'insert'] });
this.input.$input.on('keydown', e => {
if (e.which !== 27 || !this.input.getValue()) return;
e.stopPropagation();
this.input.setValue('');
});
this.input.$indicator.on('click', () => {
this.input.setValue('').focus();
});
this.$status = $('<div>').addClass('ipainput-status');
this.$parserOutput = $('<div>').attr({
class: 'mw-parser-output mw-body-content',
lang: 'en'
}).on('click', '.ipainput-symbol', e => {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey) return;
e.preventDefault();
e.stopPropagation();
if (e.currentTarget.classList.contains('ipainput-symbol-disabled')) {
return;
}
let $target = $(e.currentTarget).clone().find('.IPA');
if (!$target.length) $target = $target.end();
$target.find('.reference').remove();
let ins = $target.text().trim()
.replace(/◌|^[\(\/\[]+\s*(?=\S)|(\S)\s*[\)\/\]]+$/g, '$1');
if (e.currentTarget.classList.contains('ipainput-symbol-dia') &&
this.diaOnlyButton.getValue()
) {
let match = ins.normalize('NFD').match(/[^̧\P{Mn}]+/u);
if (match) ins = match[0];
}
let start = this.input.$input.prop('selectionStart');
let end = this.input.$input.prop('selectionEnd');
let text = this.input.getValue();
let pos = start + ins.length;
let newText = text.slice(0, start) + ins + text.slice(end);
this.input.setValue(newText).selectRange(pos).focus();
if (this.undoCache.length
? text !== this.undoCache[this.undoCache.length - 1][0]
: text
) {
this.undoCache.push([text, end]);
}
this.undoCache.push([newText, pos]);
this.undoCache = this.undoCache.slice(-500);
}).on('keydown', '.ipainput-symbol', function (e) {
if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey) return;
if (e.which === 13 || e.which === 32) {
e.preventDefault();
e.stopPropagation();
this.click();
this.focus();
}
});
this.undoButton = new OO.ui.ButtonWidget({
icon: 'undo',
invisibleLabel: true,
label: 'Undo',
classes: ['ipainput-undo']
}).on('click', () => {
let arr = this.undoCache.pop();
if (!arr) {
this.input.setValue('').focus();
return;
}
if (this.undoCache.length && this.input.getValue() === arr[0]) {
arr = this.undoCache.pop();
}
this.input.setValue(arr[0]).selectRange(arr[1]).focus();
});
this.diaOnlyButton = new OO.ui.ToggleButtonWidget({
icon: 'searchDiacritics',
invisibleLabel: true,
label: 'Insert diacrtics only',
classes: ['ipainput-diaonly']
}).on('change', enabled => {
this.$parserOutput.find(
'.ipainput-symbol:not(.ipainput-symbol-dia)'
).attr({
tabindex: enabled ? -1 : 0,
'aria-disabled': enabled ? 'true' : null
}).toggleClass('ipainput-symbol-disabled', enabled);
});
this.input.$element.append(
this.undoButton.$element, this.diaOnlyButton.$element
);
this.$transcription = $([
this.input.$element[0], this.$status[0], this.$parserOutput[0]
]);
};
IpaInputDialog.prototype.getSetupProcess = function (data) {
return IpaInputDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
let storage = (mw.storage.get('ipainput') || '').split('|');
let key = langs.hasOwnProperty(storage[0])
? storage[0]
: 'English';
this.keyDropdown.getMenu().selectItemByLabel(key);
if (storage[1] === 'null') {
this.noTemplateCheckbox.setSelected(true);
} else if (langs[key].some(([k]) => k === storage[1])) {
this.languageDropdown.getMenu().selectItemByData(storage[1]);
}
this.rememberCheckbox.setSelected(storage[0]);
this.actions.setMode('config');
}, this);
};
IpaInputDialog.prototype.getKey = function () {
let isGeneric = this.keyName === '(Full IPA chart)';
let pageName = isGeneric
? 'International Phonetic Alphabet chart'
: 'Help:IPA/' + this.keyName;
this.actions.get()[3].setDisabled(true);
this.pushPending();
this.input.setValue('');
this.undoCache = [];
this.$status.empty().append(
'Loading ',
$('<a>').attr({
href: mw.util.getUrl(pageName),
target: '_blank'
}).text(pageName),
'...'
);
this.$parserOutput.empty();
$.get(
'//en.wikipedia.org/api/rest_v1/page/html/' +
encodeURIComponent(pageName.replace(/ /g, '_'))
).then(data => {
this.curKeyName = this.keyName;
let $data = $($.parseHTML(data));
let $tables = $data.filter('section').children().unwrap();
if (isGeneric) {
$tables = $tables.filter('h2#Vowels').nextUntil('#See_also').addBack();
} else {
$tables = $tables.filter(function () {
return this.classList.contains('wikitable') ||
this.querySelector('.wikitable');
});
if ($tables.length > 1) {
$tables = $tables.first().nextUntil($tables.last().next()).addBack();
}
}
$tables.find('.IPA').filter(function () {
return /[\s,~]/.test(this.textContent.trim()) || this.querySelector('br');
}).find('*').addBack().contents().filter(function () {
return this.nodeType === 3;
}).replaceWith(function () {
return this.textContent.split(/([\s,~]+)/).reduce((acc, s, i) => {
if (s) {
acc.push(i % 2 ? s : $('<span>').attr({
class: 'ipainput-symbol',
tabindex: 0,
role: 'button'
}).text(s));
}
return acc;
}, []);
});
$tables.find('td, th').filter(function () {
if (this.querySelector('.ipainput-symbol, .IPA-vowels-container')) return;
return this.classList.contains('IPA') ||
this.querySelector('.IPA') &&
!this.querySelector('br, p') &&
!$(this).find(':not(.IPA, .IPA *, .reference, .reference *)').addBack().contents().get()
.some(n => n.nodeType === 3 && n.textContent.trim());
}).addClass('ipainput-symbol').attr({
tabindex: 0,
role: 'button'
});
let $spans = $tables.find('.IPA:not(.ipainput-symbol, .ipainput-symbol .IPA)').filter(function () {
return !this.querySelector('.ipainput-symbol');
});
let consec = [];
$spans.filter(function (i) {
if ($spans[i + 1] === this.nextSibling) {
consec.push(this);
} else if (consec.length) {
consec.push(this);
$(consec).wrapAll('<span>').parent()
.addClass('ipainput-symbol').attr({
tabindex: 0,
role: 'button'
});
consec.length = 0;
} else {
return true;
}
}).addClass('ipainput-symbol').attr({
tabindex: 0,
role: 'button'
});
$tables.find('[id], [about]').addBack().removeAttr('id about');
$tables.find('a').attr({
target: '_blank',
tabindex: -1
}).filter('[href^="./"]').attr('href', function () {
return '//en.wikipedia.org/wiki' +
this.getAttribute('href').slice(1);
});
let hasDia = $tables.find('.ipainput-symbol').filter(function () {
return /[^̧\P{Mn}]/u.test(this.textContent.normalize('NFD'));
}).addClass('ipainput-symbol-dia').length && true;
this.diaOnlyButton.setValue().toggle(hasDia);
let modules = (
$data.filter('meta[property="mw:moduleStyles"]').attr('content') || ''
).split('|');
return mw.loader.using(modules).always(() => {
this.$status.html(this.$status.children());
this.$parserOutput.append($tables);
this.updateSize();
this.actions.get()[3].setDisabled();
});
}, data => {
let msg = ((data || {}).responseJSON || {}).title;
if (msg && data.responseJSON.detail) {
msg += ': ' + data.responseJSON.detail;
}
this.$status.text(msg || 'Unknown error');
}).always(() => {
this.popPending();
});
};
IpaInputDialog.prototype.getActionProcess = function (action) {
if (action === 'transcribe') {
this.keyName = this.keyDropdown.getMenu().findSelectedItem().getLabel();
this.langCode = this.noTemplateCheckbox.isSelected()
? null
: this.languageDropdown.getMenu().findSelectedItem().getData();
this.actions.setMode('transcription');
this.form.toggle(false).$element.after(this.$transcription);
this.setSize('larger');
if (this.keyName !== this.curKeyName) {
this.getKey();
}
$(document).on('keydown.ipaInput', e => {
if (e.shiftKey || e.altKey) return;
if (e.which === 90 &&
[e.ctrlKey, e.metaKey].filter(Boolean).length === 1
) {
e.preventDefault();
this.undoButton.emit('click');
}
});
this.input.focus();
mw.requestIdleCallback(() => {
mw.storage.remove('IpaInput-keyName');
mw.storage.remove('IpaInput-template');
if (this.rememberCheckbox.isSelected()) {
mw.storage.set(
'ipainput',
this.keyName + '|' + this.langCode,
31556952
);
} else {
mw.storage.remove('ipainput');
}
});
} else {
$(document).off('keydown.ipaInput');
this.actions.setMode('config');
this.$transcription.detach();
this.form.toggle(true);
this.setSize('small');
if (action === 'insert') {
let text = this.input.getValue().trim(), template;
if (this.keyName === 'English') {
text = text
.replace(/\s+/g, '_')
.replace(/a[ɪʊ]ər|ɔɪər|[ɛɪʊ]ə[ˈˌ]r|\.\.\.|[ɑɔɜ]ːr|[ɛɪʊ]ər|!!|,_|a[ɪʊ]|[dlnstzθ]j(?=u|ʊə)|dʒ|eɪ|hw|[iuɑɔɜ]ː|oʊ|tʃ|[æɒɛɪʊʌ]r|[æɒ]̃|ɔɪ|(?:(?<=[bdfkprstvxzðɡʃʒʔθ]\.?)ə[ln]|(?<=[fsvzðʃʒθ]\.?)əm|ər)(?![aeæɑɒɔɛɜʊʌˈˌ]|[iu]ː|ɪə)|[!#\(\)\-\._bdfhijklmnprstuvwxzæðŋɒəɛɡɪʃʊʌʒʔˈˌθ]/g, '$&|')
.replace(/\|$/, '');
template = 'IPAc-en';
} else {
template = 'IPA';
if (this.langCode) {
text = this.langCode + '|' + text;
}
}
if (document.documentElement.classList.contains('ve-active')) {
text = this.langCode === null ? text : [{
type: 'mwTransclusionInline',
attributes: {
mw: {
parts: [{
template: {
target: { wt: template },
params: text.split('|').reduce((acc, s, i) => {
acc[i + 1] = { wt: s };
return acc;
}, {})
}
}]
}
}
}];
ve.init.target.getSurface().getModel().getFragment().collapseToEnd()
.insertContent(text).collapseToEnd().select();
} else {
if (this.langCode !== null) {
text = `{{${template}|${text}}}`;
}
$('#wpTextbox1').textSelection('encapsulateSelection', {
peri: text,
replace: true
});
}
this.input.setValue('');
this.undoCache = [];
this.close();
}
}
return IpaInputDialog.super.prototype.getActionProcess.call(this, action);
};
window.ipaInputDialog = new IpaInputDialog();
let winMan = new OO.ui.WindowManager();
winMan.addWindows([window.ipaInputDialog]);
winMan.$element.appendTo(OO.ui.getTeleportTarget());
window.ipaInputDialog.open();
});
}());