['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 }
}
}
});
});
}());