UNPKG

prism-code-editor

Version:

Lightweight, extensible code editor component for the web using Prism

228 lines (227 loc) 8.64 kB
import { a as languages, i as highlightTokens, l as tokenizeText } from "./core-DEy9UQvI.js"; //#region src/core.ts /** * Creates a code editor using the specified container and options. * @param container Element to append the editor to or a selector. * This can also be a `ShadowRoot` or `DocumentFragment` for example. * If omitted, you must manually append `editor.container` to the DOM. * @param options Options the editor is initialized with. * If omitted, the editor won't function until you call `editor.setOptions`. * @param extensions Extensions added before the first render. You can still add extensions later. * @returns Object to interact with the created editor. */ var createEditor = (container, options, ...extensions) => { let language; let prevLines = []; let activeLine; let value = ""; let activeLineNumber; let focused = false; let handleSelectionChange = true; let tokens = []; let readOnly; let lineCount = 0; const scrollContainer = editorTemplate(); const wrapper = scrollContainer.firstChild; const lines = wrapper.children; const overlays = lines[0]; const textarea = overlays.firstChild; const currentOptions = { language: "text", value }; const currentExtensions = new Set(extensions); const listeners = {}; const setOptions = (options) => { Object.assign(currentOptions, options); let isNewVal = value != (value = options.value ?? value); let isNewLang = language != (language = currentOptions.language); readOnly = !!currentOptions.readOnly; scrollContainer.style.tabSize = currentOptions.tabSize || 2; textarea.inputMode = readOnly ? "none" : ""; textarea.setAttribute("aria-readonly", readOnly); updateClassName(); updateExtensions(); if (isNewVal) { if (!focused) textarea.remove(); textarea.value = value; textarea.selectionEnd = 0; if (!focused) overlays.prepend(textarea); } if (isNewVal || isNewLang) update(); }; const update = () => { tokens = tokenizeText(value = textarea.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]}\n</div>`; for (i = end1 < start ? end1 : start - 1; i < end2; i++) lines[start + 1].remove(); if (newHTML) lines[insertStart + 1].insertAdjacentHTML("afterend", newHTML); scrollContainer.style.setProperty("--number-width", (0 | Math.log10(lineCount)) + 1 + ".001ch"); } dispatchEvent("update", value); dispatchSelection(true); if (handleSelectionChange) setTimeout(setTimeout, 0, () => handleSelectionChange = true); prevLines = newLines; handleSelectionChange = false; }; const updateExtensions = (newExtensions) => { (newExtensions || currentExtensions).forEach((extension) => { if (typeof extension == "object") { extension.update(self, currentOptions); if (newExtensions) currentExtensions.add(extension); } else { extension(self, currentOptions); if (!newExtensions) currentExtensions.delete(extension); } }); }; const updateClassName = ([start, end] = getInputSelection()) => { scrollContainer.className = `prism-code-editor language-${language}${currentOptions.lineNumbers == false ? "" : " show-line-numbers"} pce-${currentOptions.wordWrap ? "" : "no"}wrap${currentOptions.rtl ? " pce-rtl" : ""} pce-${start < end ? "has" : "no"}-selection${focused ? " pce-focus" : ""}${readOnly ? " pce-readonly" : ""}${currentOptions.class ? " " + currentOptions.class : ""}`; }; const getInputSelection = () => [ textarea.selectionStart, textarea.selectionEnd, textarea.selectionDirection ]; const keyCommandMap = { Escape() { textarea.blur(); } }; const inputCommandMap = {}; const dispatchEvent = (name, ...args) => { listeners[name]?.forEach((handler) => handler.apply(self, args)); currentOptions["on" + name[0].toUpperCase() + name.slice(1)]?.(...args, self); }; const dispatchSelection = (force) => { if (force || handleSelectionChange) { const selection = getInputSelection(); const newLine = 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; } updateClassName(selection); dispatchEvent("selectionChange", selection, value); } }; const self = { container: scrollContainer, wrapper, lines, textarea, get activeLine() { return activeLineNumber; }, get value() { return value; }, options: currentOptions, get focused() { return focused; }, get tokens() { return tokens; }, inputCommandMap, keyCommandMap, extensions: {}, setOptions, update, getSelection: getInputSelection, addExtensions(...extensions) { updateExtensions(extensions); }, on: (name, handler) => { (listeners[name] ||= /* @__PURE__ */ new Set()).add(handler); return () => listeners[name].delete(handler); }, remove() { scrollContainer.remove(); } }; addListener(textarea, "keydown", (e) => { keyCommandMap[e.key]?.(e, getInputSelection(), value) && preventDefault(e); }); addListener(textarea, "beforeinput", (e) => { if (readOnly || e.inputType == "insertText" && inputCommandMap[e.data]?.(e, getInputSelection(), value)) preventDefault(e); }); addListener(textarea, "input", update); addListener(textarea, "blur", () => { selectionChange = null; focused = false; updateClassName(); }); addListener(textarea, "focus", () => { selectionChange = dispatchSelection; focused = true; updateClassName(); }); addListener(textarea, "selectionchange", (e) => { dispatchSelection(!e.isTrusted); preventDefault(e); }); getElement(container)?.append(scrollContainer); options && setOptions(options); return self; }; /** * Almost identical to {@link createEditor}, but instead of appending the editor to your * element, the editor replaces it. * * The `textContent` of the placeholder will be the code in the editor unless `options.value` is defined. * @param placeholder Node or selector which will be replaced by the editor. * @param options Options the editor is initialized with. * @param extensions Extensions added before the first render. You can still add extensions later. * @returns Object to interact with the created editor. */ var editorFromPlaceholder = (placeholder, options, ...extensions) => { const el = getElement(placeholder); const editor = createEditor(null, Object.assign({ value: el.textContent }, options), ...extensions); el.replaceWith(editor.container); return editor; }; /** Equivalent to `document` in a browser setting, `null` otherwise. */ var doc = "u" > typeof window ? document : null; var templateEl = /* @__PURE__ */ doc?.createElement("div"); var createTemplate = (html, node) => { if (templateEl) { templateEl.innerHTML = html; node = templateEl.firstChild; } return () => node.cloneNode(true); }; var addListener = (target, type, listener, options) => target.addEventListener(type, listener, options); var getElement = (el) => typeof el == "string" ? doc.querySelector(el) : el; /** * Counts number of lines in the string between `start` and `end`. * If start and end are omitted, the whole string is searched. */ var numLines = (str, start = 0, end = Infinity) => { let count = 1; for (; (start = str.indexOf("\n", start) + 1) && start <= end; count++); return count; }; /** Object storing all language specific behavior. */ var languageMap = {}; var editorTemplate = /* @__PURE__ */ createTemplate("<div><div class=pce-wrapper><div class=pce-overlays><textarea class=pce-textarea spellcheck=false autocapitalize=off autocomplete=off>"); var preventDefault = (e) => { e.preventDefault(); e.stopImmediatePropagation(); }; var setSelectionChange = (f) => selectionChange = f; var selectionChange; if (doc) addListener(doc, "selectionchange", () => selectionChange?.()); //#endregion export { editorFromPlaceholder as a, numLines as c, doc as i, preventDefault as l, createEditor as n, getElement as o, createTemplate as r, languageMap as s, addListener as t, setSelectionChange as u }; //# sourceMappingURL=core-C0HOUBwg.js.map