on-codemerge
Version:
A WYSIWYG editor for on-codemerge is a user-friendly interface that allows users to edit and view their code in real time, exactly as it will appear in the final product
202 lines (201 loc) • 9.85 kB
JavaScript
/*! on-codemerge v1.3.1 @author Pavel Kuzmin @license MIT @homepage https://s00d.github.io/on-codemerge/ @repository git+https://github.com/s00d/on-codemerge.git Copyright (c) 2026 Pavel Kuzmin - Built on 2026-07-02T13:39:17.947Z */
var g = Object.defineProperty;
var C = (i, e, t) => e in i ? g(i, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : i[e] = t;
var a = (i, e, t) => C(i, typeof e != "symbol" ? e + "" : e, t);
import { createToolbarButton as k } from "../ToolbarPlugin/utils.mjs";
import x from "../../icons/spell-check.svg.mjs";
import S from "../../../node_modules/.pnpm/spellchecker-wasm@0.3.3/node_modules/spellchecker-wasm/lib/spellchecker-wasm.wasm.mjs";
const b = /* @__PURE__ */ Object.assign({
"./dictionaries/ar.txt": () => import("./dictionaries/ar.txt.mjs").then((i) => i.default),
"./dictionaries/cs.txt": () => import("./dictionaries/cs.txt.mjs").then((i) => i.default),
"./dictionaries/de.txt": () => import("./dictionaries/de.txt.mjs").then((i) => i.default),
"./dictionaries/en.txt": () => import("./dictionaries/en.txt.mjs").then((i) => i.default),
"./dictionaries/es.txt": () => import("./dictionaries/es.txt.mjs").then((i) => i.default),
"./dictionaries/fr.txt": () => import("./dictionaries/fr.txt.mjs").then((i) => i.default),
"./dictionaries/hi.txt": () => import("./dictionaries/hi.txt.mjs").then((i) => i.default),
"./dictionaries/id.txt": () => import("./dictionaries/id.txt.mjs").then((i) => i.default),
"./dictionaries/it.txt": () => import("./dictionaries/it.txt.mjs").then((i) => i.default),
"./dictionaries/ja.txt": () => import("./dictionaries/ja.txt.mjs").then((i) => i.default),
"./dictionaries/ko.txt": () => import("./dictionaries/ko.txt.mjs").then((i) => i.default),
"./dictionaries/nl.txt": () => import("./dictionaries/nl.txt.mjs").then((i) => i.default),
"./dictionaries/pl.txt": () => import("./dictionaries/pl.txt.mjs").then((i) => i.default),
"./dictionaries/pt.txt": () => import("./dictionaries/pt.txt.mjs").then((i) => i.default),
"./dictionaries/ru.txt": () => import("./dictionaries/ru.txt.mjs").then((i) => i.default),
"./dictionaries/th.txt": () => import("./dictionaries/th.txt.mjs").then((i) => i.default),
"./dictionaries/tr.txt": () => import("./dictionaries/tr.txt.mjs").then((i) => i.default),
"./dictionaries/vi.txt": () => import("./dictionaries/vi.txt.mjs").then((i) => i.default),
"./dictionaries/zh.txt": () => import("./dictionaries/zh.txt.mjs").then((i) => i.default)
});
class M {
constructor() {
a(this, "name", "spellchecker");
a(this, "hotkeys", [
{
keys: "Ctrl+Shift+S",
description: "Run spell checker",
command: "spellchecker",
icon: "🔍✅"
}
]);
a(this, "editor", null);
a(this, "toolbarButton", null);
a(this, "isSpellCheckEnabled", !1);
a(this, "spellChecker", null);
a(this, "misspelledWords", /* @__PURE__ */ new Map());
a(this, "lastResults", []);
}
async initialize(e) {
this.editor = e, this.addToolbarButton(), this.enableSpellCheck(this.isSpellCheckEnabled), this.editor.on("spellchecker", async () => {
var t, s;
this.isSpellCheckEnabled = !this.isSpellCheckEnabled, this.isSpellCheckEnabled && await this.loadDictionary(((t = this.editor) == null ? void 0 : t.getLocale()) ?? "en"), this.enableSpellCheck(this.isSpellCheckEnabled), (s = this.toolbarButton) == null || s.classList.toggle("active", this.isSpellCheckEnabled);
});
}
async loadDictionary(e) {
try {
const t = await fetch(S), s = b[`./dictionaries/${e}.txt`];
if (!s)
throw new Error(`Dictionary files for locale ${e} not found`);
const l = await s(), n = await fetch(l);
if (!t.ok || !n.ok)
throw new Error("Failed to load dictionary files");
const { SpellcheckerWasm: o } = await import("../../../_virtual/index.mjs").then((r) => r.i);
this.spellChecker = new o((r) => {
this.lastResults = r;
}), await this.spellChecker.prepareSpellchecker(t, n);
} catch (t) {
console.error("Failed to load dictionary:", t), this.spellChecker = null;
}
}
addToolbarButton() {
var t, s;
const e = (t = this.editor) == null ? void 0 : t.getToolbar();
e && (this.toolbarButton = k({
icon: x,
title: (s = this.editor) == null ? void 0 : s.t("Spell Check"),
onClick: async () => {
var l, n;
this.isSpellCheckEnabled = !this.isSpellCheckEnabled, this.isSpellCheckEnabled && await this.loadDictionary(((l = this.editor) == null ? void 0 : l.getLocale()) ?? "en"), this.enableSpellCheck(this.isSpellCheckEnabled), (n = this.toolbarButton) == null || n.classList.toggle("active", this.isSpellCheckEnabled);
}
}), e.appendChild(this.toolbarButton));
}
enableSpellCheck(e) {
var s;
((s = this.editor) == null ? void 0 : s.getContainer()) && (e ? (this.addSpellCheckListeners(), this.checkAllText()) : (this.removeSpellCheckListeners(), this.clearHighlights()));
}
addSpellCheckListeners() {
var t;
const e = (t = this.editor) == null ? void 0 : t.getContainer();
e && (e.addEventListener("input", this.handleInput.bind(this)), e.addEventListener("mouseup", this.handleMouseUp.bind(this)), e.addEventListener("contextmenu", this.handleContextMenu.bind(this)));
}
removeSpellCheckListeners() {
var t;
const e = (t = this.editor) == null ? void 0 : t.getContainer();
e && (e.removeEventListener("input", this.handleInput.bind(this)), e.removeEventListener("mouseup", this.handleMouseUp.bind(this)), e.removeEventListener("contextmenu", this.handleContextMenu.bind(this)));
}
handleInput(e) {
this.checkAllText();
}
handleMouseUp(e) {
var s, l;
const t = (l = (s = this.editor) == null ? void 0 : s.getTextFormatter()) == null ? void 0 : l.getSelection();
if (t && t.rangeCount > 0) {
const n = t.getRangeAt(0);
n.toString() && this.checkSpelling(n);
}
}
handleContextMenu(e) {
var s, l;
const t = (l = (s = this.editor) == null ? void 0 : s.getTextFormatter()) == null ? void 0 : l.getSelection();
if (t && t.rangeCount > 0) {
const n = t.getRangeAt(0), o = n.toString();
if (o && this.isMisspelled(o)) {
e.preventDefault();
const r = this.getSpellingSuggestions(o);
r.length > 0 && this.showContextMenu(e, r, n);
}
}
}
checkAllText() {
var l;
const e = (l = this.editor) == null ? void 0 : l.getContainer();
if (!e) return;
this.clearHighlights();
const t = document.createTreeWalker(e, NodeFilter.SHOW_TEXT);
let s;
for (; s = t.nextNode(); ) {
const n = s, o = n.textContent;
if (o) {
const r = o.split(/\s+/);
let d = 0;
r.forEach((h) => {
if (this.isMisspelled(h)) {
const c = document.createRange();
c.setStart(n, d), c.setEnd(n, d + h.length), this.highlightMisspelledWord(n, c);
}
d += h.length + 1;
});
}
}
}
checkSpelling(e) {
const t = e.toString();
if (this.isMisspelled(t)) {
const s = e.startContainer;
this.highlightMisspelledWord(s, e);
}
}
isMisspelled(e) {
return this.spellChecker ? (this.lastResults = [], this.spellChecker.checkSpelling(e, {
includeUnknown: !0,
// Включаем слова, которых нет в словаре
maxEditDistance: 2,
// Максимальное расстояние редактирования
verbosity: 1,
// Уровень детализации предложений
includeSelf: !1
// Не включать само слово в результаты
}), this.lastResults.length > 0) : !1;
}
getSpellingSuggestions(e) {
return this.spellChecker ? (this.lastResults = [], this.spellChecker.checkSpelling(e, {
includeUnknown: !0,
maxEditDistance: 2,
verbosity: 2,
// Возвращаем все предложения
includeSelf: !1
}), this.lastResults.map((t) => t.term)) : [];
}
highlightMisspelledWord(e, t) {
var s;
(s = e.parentElement) == null || s.classList.add("misspelled-word"), this.misspelledWords.set(e, t);
}
clearHighlights() {
this.misspelledWords.forEach((e, t) => {
var s;
(s = t.parentElement) == null || s.classList.remove("misspelled-word");
}), this.misspelledWords.clear();
}
showContextMenu(e, t, s) {
var u, f;
const l = document.createElement("div");
l.style.position = "fixed", l.style.backgroundColor = "#fff", l.style.border = "1px solid #ccc", l.style.boxShadow = "0 2px 5px rgba(0, 0, 0, 0.2)", l.style.zIndex = "1000", l.style.maxHeight = "200px", l.style.overflowY = "auto", l.style.padding = "4px 0";
const n = 200, o = Math.min(t.length * 32, 200), r = window.innerWidth, d = window.innerHeight;
let h = e.clientY, c = e.clientX;
h + o > d && (h = d - o), c + n > r && (c = r - n), l.style.top = `${h}px`, l.style.left = `${c}px`, t.forEach((m) => {
const p = document.createElement("div");
p.style.padding = "8px 16px", p.style.cursor = "pointer", p.textContent = m, p.addEventListener("click", () => {
this.replaceMisspelledWord(s, m), l.remove();
}), l.appendChild(p);
}), (u = this.editor) == null || u.getInnerContainer().appendChild(l), (f = this.editor) == null || f.getDOMContext().addEventListener("click", () => l.remove(), { once: !0 });
}
replaceMisspelledWord(e, t) {
e.deleteContents(), e.insertNode(document.createTextNode(t)), this.checkAllText();
}
destroy() {
var e;
this.enableSpellCheck(!1), (e = this.toolbarButton) == null || e.remove(), this.toolbarButton = null, this.editor = null, this.spellChecker = null;
}
}
export {
M as SpellCheckerPlugin
};