User:Nardog/IPAInput-core.js

From Wikipedia, the free encyclopedia
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();
	});
}());