Jump to content

User:Nardog/InsertAnyChar.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.
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(function () {
	let clicked, dialog;
	let openDialog = async context => {
		if (clicked) {
			if (dialog) {
				dialog.context = context;
				dialog.open();
			}
			return;
		}
		clicked = true;
		mw.loader.addStyleTag(`.oo-ui-windowManager-floating > .insertanychar > .oo-ui-window-frame{margin-top:0 !important;margin-inline-end:0 !important} .insertanychar{cursor:pointer} .insertanychar-item{padding-inline:0} .insertanychar-item > .oo-ui-labelElement-label{display:flex !important;overflow:visible !important;align-items:center} .insertanychar-char{font-size:200%;min-width:1em;line-height:1;margin-inline:8px;text-align:center;font-family:'Charis SIL','Doulos SIL','Gentium','Gentium Plus','Noto Serif','Liberation Serif','Roboto Serif',serif,'Andika','Noto Sans','Liberation Sans','Roboto Sans','Bitstream Cyberbit','Code2000','Lucida Grande','Lucida Sans Unicode',sans-serif} .insertanychar-char::after{content:var(--insertanychar-vs)} .insertanychar-name{white-space:normal !important;word-break:break-word;flex-grow:1} .insertanychar-item > .oo-ui-labelElement-label::after{content:attr(data-insertanychar);font-size:85%;margin-inline:4px;color:var(--color-subtle,#54595d)}`);
		let promise = mw.loader.using([
			'oojs-ui-core', 'oojs-ui-windows', 'jquery.textSelection'
		]);
		let response = await $.get('//en.wiktionary.org/w/rest.php/v1/revision/80968402');
		// https://www.unicode.org/versions/latest/core-spec/chapter-3/
		let hangul = {
			leads: [
				'G', 'GG', 'N', 'D', 'DD', 'R', 'M', 'B', 'BB', 'S', 'SS', '',
				'J', 'JJ', 'C', 'K', 'T', 'P', 'H'
			],
			vowels: [
				'A', 'AE', 'YA', 'YAE', 'EO', 'E', 'YEO', 'YE', 'O', 'WA',
				'WAE', 'OE', 'YO', 'U', 'WEO', 'WE', 'WI', 'YU', 'EU', 'YI', 'I'
			],
			trails: [
				'', 'G', 'GG', 'GS', 'N', 'NJ', 'NH', 'D', 'L', 'LG', 'LM',
				'LB', 'LS', 'LT', 'LP', 'LH', 'M', 'B', 'BS', 'S', 'SS', 'NG',
				'J', 'C', 'K', 'T', 'P', 'H'
			]
		};
		let data = [], vs = [], blockStart;
		response.source.split('\n').forEach(s => {
			let props = s.split(';');
			let v = parseInt(props[0], 16);
			let name = props[1];
			if (name?.[0] === '<') {
				if (name === '<control>') {
					name = props[10];
				} else {
					let blockName = name.match(/[^,<>]+/)?.[0].toUpperCase();
					if (!blockName || blockName.endsWith(' SURROGATE')) return;
					if (blockStart) {
						if (blockName === 'HANGUL SYLLABLE') {
							for (let i = 0; blockStart + i <= v; i++) {
								data.push([
									blockName + ' ' +
										hangul.leads[Math.floor(i / 588)] +
										hangul.vowels[Math.floor((i % 588) / 28)] +
										hangul.trails[i % 28],
									blockStart + i
								]);
							}
						} else {
							data.push([blockName, [blockStart, v]]);
						}
						blockStart = null;
					} else {
						blockStart = v;
					}
					return;
				}
			}
			if (!name) return;
			if (props[2] === 'Mn') {
				data.push([name, v, true, ['233', '234'].includes(props[3])]);
				if (name.startsWith('VARIATION SELECTOR-')) {
					vs.push(v);
				}
			} else {
				data.push([name, v]);
			}
		});
		await promise;
		function InsertAnyCharDialog(config) {
			InsertAnyCharDialog.super.call(this, config);
			this.$element.addClass('insertanychar');
		}
		OO.inheritClass(InsertAnyCharDialog, OO.ui.Dialog);
		InsertAnyCharDialog.static.name = 'insertAnyCharDialog';
		InsertAnyCharDialog.prototype.initialize = function () {
			InsertAnyCharDialog.super.prototype.initialize.call(this);
			this.$element.on('click', e => {
				if (!this.$content[0].contains(e.target) &&
					!this.$overlay[0].contains(e.target)
				) {
					this.close();
				}
			});
			this.moreButton = new OO.ui.MenuOptionWidget({
				label: 'Load more'
			});
			new IntersectionObserver(entries => {
				if (entries[0].isIntersecting) {
					this.continue();
				}
			}, { threshold: 0.75 }).observe(this.moreButton.$element[0]);
			this.input = new OO.ui.SearchInputWidget({
				autocomplete: false
			}).on('change', OO.ui.debounce(value => {
				this.menu.toggle(false).clearItems();
				this.res = value.toUpperCase().split(/[^\dA-Z]+/).filter(Boolean)
					.map(s => new RegExp('\\b' + s));
				if (!this.res.length) return;
				this.startFrom = 0;
				this.startBlockFrom = 0;
				this.lookUp();
			}, 250));
			this.menu = new OO.ui.MenuSelectWidget({
				$floatableContainer: this.$body,
				autoHide: false,
				hideOnChoose: false,
				input: this.input,
				widget: this.input
			}).on('choose', item => {
				if (item === this.moreButton) {
					this.continue();
					return;
				}
				this.context.$textarea.textSelection('encapsulateSelection', {
					peri: String.fromCodePoint(parseInt(item.$label.data('insertanychar'), 16)),
					selectPeri: false
				});
				this.menu.unselectItem();
			});
			this.manager.connect(this.menu, { resize: 'position' })
				.connect(this.menu, { resize: 'clip' });
			this.$overlay.append(this.menu.$element);
			this.vsDropdown = new OO.ui.DropdownWidget({
				$overlay: this.$overlay,
				menu: {
					items: [
						new OO.ui.MenuOptionWidget({
							data: '',
							label: 'None'
						}),
						...vs.map((v, i) => new OO.ui.MenuOptionWidget({
							data: v,
							label: `${i + 1} (${v.toString(16).toUpperCase()})`
						}))
					]
				}
			});
			this.vsDropdown.getMenu().selectItemByData('').on('choose', option => {
				let v = option.getData();
				this.menu.$element.css(
					'--insertanychar-vs',
					v ? `'\\${v.toString(16)}'` : ''
				);
			});
			this.form = new OO.ui.FormLayout({
				items: [
					new OO.ui.FieldLayout(this.vsDropdown, {
						label: 'Variation selector in preview:'
					}),
					this.input
				]
			});
			this.$body.append(this.form.$element);
		};
		InsertAnyCharDialog.prototype.lookUp = function () {
			this.busy = true;
			let matches = [];
			let halted = data.slice(this.startFrom).some(entry => {
				if (typeof entry[1] === 'number') {
					this.startFrom++;
					if (this.res.every(re => re.test(entry[0]))) {
						return matches.push(entry) === 100;
					}
				} else if (this.res.every(re => (
					re.test(entry[0]) || /^\\b[\dA-F]+$/.test(re.source)
				))) {
					for (let i = entry[1][0] + this.startBlockFrom; i <= entry[1][1]; i++) {
						this.startBlockFrom++;
						let hex = i.toString(16).toUpperCase();
						if (this.res.every(re => re.test(entry[0]) || re.test(hex)) &&
							matches.push([entry[0], i]) === 100
						) {
							return true;
						}
					}
					this.startBlockFrom = 0;
					this.startFrom++;
				}
			});
			if (!matches.length) return;
			let items = matches.map(([n, v, combining, isDouble]) => new OO.ui.MenuOptionWidget({
				$label: $('<span>').attr(
					'data-insertanychar',
					v.toString(16).toUpperCase().padStart(4, '0')
				),
				classes: ['insertanychar-item'],
				label: $('<span>').addClass('insertanychar-char').text(
					`${combining ? '◌' : ''}${
						String.fromCodePoint(v)
					}${isDouble ? '◌' : ''}`
				).add($('<span>').addClass('insertanychar-name').text(
					n.replace(
						/ (?:AND|AS|AT|BY|FOR|FROM|IN|OF|ON|OR|THE|TO|WITH)(?= )/g,
						s => s.toLowerCase()
					).replace(
						/\b(?!CJK\b)[\dA-Z]{2,}\b/g,
						s => s[0] + s.slice(1).toLowerCase()
					)
				))
			}));
			if (halted) {
				items.push(this.moreButton);
			}
			this.menu.addItems(items).toggle(true);
			setTimeout(() => {
				this.busy = false;
			}, 500);
		};
		InsertAnyCharDialog.prototype.continue = function () {
			if (this.busy) return;
			this.menu.removeItems([this.moreButton]);
			this.lookUp();
		};
		InsertAnyCharDialog.prototype.getReadyProcess = function () {
			return InsertAnyCharDialog.super.prototype.getReadyProcess.apply(this, arguments).next(function () {
				this.input.focus();
				this.menu.position().clip();
			}, this);
		};
		dialog = new InsertAnyCharDialog();
		dialog.context = context;
		let winMan = new OO.ui.WindowManager();
		winMan.addWindows([dialog]);
		winMan.$element.appendTo(OO.ui.getTeleportTarget());
		dialog.open();
	};
	mw.hook('wikiEditor.toolbarReady').add($textarea => {
		$textarea.wikiEditor('addToToolbar', {
			section: 'main',
			group: 'insert',
			tools: {
				dialog: {
					label: 'InsertAnyChar',
					type: 'button',
					oouiIcon: 'specialCharacter',
					action: { type: 'callback', execute: openDialog }
				}
			}
		});
	});
}());