prism-react-editor
Version:
Lightweight, extensible code editor component for React apps
222 lines (221 loc) • 7.78 kB
JavaScript
import { jsx, jsxs } from "react/jsx-runtime";
import { t as tokenizeText, l as languages, h as highlightTokens } from "./index-k28m3HFc.js";
import { memo, forwardRef, useLayoutEffect, useImperativeHandle, useRef } from "react";
const Editor = memo(
forwardRef((props, ref) => {
let prevLines = [];
let lineCount = 0;
let lines;
let activeLine;
let language;
let activeLineNumber = 0;
let value = "";
let prevVal;
let prevClass;
let focused = false;
let tokens = [];
let textarea;
let container;
const getInputSelection = () => textarea ? [textarea.selectionStart, textarea.selectionEnd, textarea.selectionDirection] : [0, 0, "none"];
const listeners = {};
const keyCommandMap = {
Escape() {
textarea.blur();
}
};
const inputCommandMap = {};
const updateSelection = (force) => {
if (handleSelectionChange || force) {
const selection = getInputSelection();
const newLine = editor.lines[activeLineNumber = numLines(value, 0, selection[selection[2] < "f" ? 0 : 1])];
if (newLine != activeLine) {
activeLine?.classList.remove("active-line");
newLine.classList.add("active-line");
activeLine = newLine;
}
updateClass();
dispatchEvent("selectionChange", selection, value);
}
};
const dispatchEvent = (name, ...args) => {
listeners[name]?.forEach((handler) => handler(...args));
editor.props["on" + name[0].toUpperCase() + name.slice(1)]?.(...args, editor);
};
const updateClass = useStableRef(() => {
let props2 = editor.props;
let [start, end] = getInputSelection();
let classProp = props2.className;
let newClass = `prism-code-editor language-${language}${props2.lineNumbers == false ? "" : " show-line-numbers"} pce-${props2.wordWrap ? "" : "no"}wrap${props2.rtl ? " pce-rtl" : ""} pce-${start < end ? "has" : "no"}-selection${focused ? " pce-focus" : ""}${props2.readOnly ? " pce-readonly" : ""}${classProp ? " " + classProp : ""}`;
if (newClass != prevClass) container.className = prevClass = newClass;
});
const update = () => {
value = textarea.value;
tokens = tokenizeText(value, languages[language] || {});
dispatchEvent("tokenize", tokens, language, value);
let newLines = highlightTokens(tokens).split("\n");
let start = 0;
let end2 = lineCount;
let end1 = lineCount = newLines.length;
while (newLines[start] == prevLines[start] && start < end1) ++start;
while (end1 && newLines[--end1] == prevLines[--end2]) ;
if (start == end1 && start == end2) lines[start + 1].innerHTML = newLines[start] + "\n";
else {
let insertStart = end2 < start ? end2 : start - 1;
let i = insertStart;
let newHTML = "";
while (i < end1) newHTML += `<div class=pce-line aria-hidden=true>${newLines[++i]}
</div>`;
for (i = end1 < start ? end1 : start - 1; i < end2; i++) lines[start + 1].remove();
if (newHTML) lines[insertStart + 1].insertAdjacentHTML("afterend", newHTML);
for (i = insertStart + 1; i < lineCount; ) lines[++i].setAttribute("data-line", i);
container.style.setProperty(
"--number-width",
Math.ceil(Math.log10(lineCount + 1)) + ".001ch"
);
}
dispatchEvent("update", value);
updateSelection(true);
if (handleSelectionChange) setTimeout(setTimeout, 0, () => handleSelectionChange = true);
prevLines = newLines;
handleSelectionChange = false;
};
const editor = useStableRef({
inputCommandMap,
keyCommandMap,
extensions: {},
get value() {
return value;
},
get focused() {
return focused;
},
get tokens() {
return tokens;
},
get activeLine() {
return activeLineNumber;
},
on: (name, listener) => {
(listeners[name] ||= /* @__PURE__ */ new Set()).add(listener);
return () => {
listeners[name].delete(listener);
};
},
update,
getSelection: getInputSelection
});
const textareaRef = useStableRef((el) => {
if (el && !textarea) {
editor.textarea = textarea = el;
addListener(textarea, "keydown", (e) => {
keyCommandMap[e.key]?.(e, getInputSelection(), value) && preventDefault(e);
});
addListener(textarea, "beforeinput", (e) => {
if (editor.props.readOnly || e.inputType == "insertText" && inputCommandMap[e.data]?.(e, getInputSelection(), value))
preventDefault(e);
});
addListener(textarea, "input", update);
addListener(textarea, "blur", () => {
selectionChange = null;
focused = false;
updateClass();
});
addListener(textarea, "focus", () => {
selectionChange = updateSelection;
focused = true;
updateClass();
});
addListener(textarea, "selectionchange", (e) => {
updateSelection();
preventDefault(e);
});
}
});
editor.props = props = { language: "text", value: "", ...props };
useLayoutEffect(
useStableRef(() => {
const { value: newVal, language: newLang } = editor.props;
if (newVal != prevVal) {
if (!focused) textarea.remove();
textarea.value = prevVal = newVal;
textarea.selectionEnd = 0;
if (!focused) lines[0].prepend(textarea);
}
language = newLang;
update();
}),
[props.value, props.language]
);
useLayoutEffect(updateClass);
useImperativeHandle(ref, () => editor, []);
return /* @__PURE__ */ jsx(
"div",
{
ref: useStableRef((el) => {
if (el) editor.container = container = el;
}),
style: {
...props.style,
tabSize: `${props.tabSize || 2}`
},
children: /* @__PURE__ */ jsx(
"div",
{
className: "pce-wrapper",
ref: useStableRef((el) => {
if (el) {
editor.wrapper = el;
editor.lines = lines = el.children;
}
}),
children: /* @__PURE__ */ jsxs("div", { className: "pce-overlays", children: [
/* @__PURE__ */ jsx(
"textarea",
{
spellCheck: "false",
autoCapitalize: "none",
autoComplete: "off",
inputMode: props.readOnly ? "none" : "text",
"aria-readonly": props.readOnly,
...props.textareaProps,
className: "pce-textarea",
ref: textareaRef
}
),
props.children?.(editor)
] })
}
)
}
);
})
);
const doc = "u" > typeof window ? document : null;
const languageMap = {};
const preventDefault = (e) => {
e.preventDefault();
e.stopImmediatePropagation();
};
const addListener = (target, type, listener, options) => target.addEventListener(type, listener, options);
const useStableRef = (value) => {
return useRef(value).current;
};
const numLines = (str, start = 0, end = Infinity) => {
let count = 1;
for (; (start = str.indexOf("\n", start) + 1) && start <= end; count++) ;
return count;
};
if (doc) addListener(doc, "selectionchange", () => selectionChange?.());
let selectionChange;
let handleSelectionChange = true;
export {
Editor as E,
addListener as a,
doc as d,
languageMap as l,
numLines as n,
preventDefault as p,
selectionChange as s,
useStableRef as u
};
//# sourceMappingURL=core-Dm5I6BkG.js.map