UNPKG

prism-react-editor

Version:

Lightweight, extensible code editor component for React apps

222 lines (221 loc) 7.78 kB
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