UNPKG

prism-react-editor

Version:

Lightweight, extensible code editor component for React apps

403 lines (402 loc) 15.9 kB
import { useLayoutEffect, useMemo, useEffect } from "react"; import { u as useStableRef, a as addListener, p as preventDefault, d as doc, n as numLines } from "./core-Dm5I6BkG.js"; import { u as useEditorSearch } from "./search-CT5tL2B1.js"; import { a as addTextareaListener, d as insertText, s as setSelection, m as scrollToEl, k as createTemplate, b as addListener2, i as isMac, q as addOverlay, u as updateNode, g as getModifierCode, h as getLineBefore, j as getLineStart, f as getLineEnd, w as isWebKit, r as regexEscape } from "./local-Cq-4Fajb.js"; import { g as getStyleValue } from "./other-EdiSn7BB.js"; const useEditorReplace = (editor, initClassName, initZIndex) => { const search = useEditorSearch(editor, initClassName, initZIndex); const getSelection = editor.getSelection; const matches = search.matches; const closest = () => { const caretPos = getSelection()[0]; const l = matches.length; for (let i = l; i; ) { if (caretPos >= matches[--i][1]) return (i + (matches[i][0] < caretPos)) % l; } return l ? 0 : -1; }; const toggleClasses = () => { currentLine?.classList.toggle("match-highlight"); currentMatch?.classList.toggle("match"); }; const removeSelection = useStableRef(() => { if (hasSelected) { toggleClasses(); hasSelected = false; } }); let currentLine; let currentMatch; let hasSelected = false; useLayoutEffect(() => { return addTextareaListener(editor, "focus", removeSelection); }, []); useLayoutEffect(() => removeSelection, []); return useMemo( () => Object.assign(search, { next() { const cursor = getSelection()[1]; const l = matches.length; for (let i = 0, match; i < l; i++) { match = matches[i]; if (match[0] - (match[0] == match[1]) >= cursor) return i; } return l ? 0 : -1; }, prev() { const cursor = getSelection()[0]; const l = matches.length; for (let i = l, match; i; ) { match = matches[--i]; if (match[1] + (match[0] == match[1]) <= cursor) return i; } return l - 1; }, closest, selectMatch(index, scrollPadding) { removeSelection(); if (matches[index]) { setSelection(editor, ...matches[index]); currentLine = editor.lines[editor.activeLine]; currentMatch = search.container.children[index]; hasSelected = true; toggleClasses(); if (currentMatch) { scrollToEl(editor, currentMatch, scrollPadding); } } }, replace(str) { if (matches[0]) { let index = closest(); let [start, end] = matches[index]; let [caretStart, caretEnd] = getSelection(); let notSelected = start != caretStart || end != caretEnd; if (notSelected) return index; if (editor.value.slice(start, end) == str) return matches[++index] ? index : 0; return insertText(editor, str); } }, replaceAll(str) { if (!matches[0]) return; let value = editor.value; let [start, end] = getSelection(); let newLen = str.length; let newStart = start; let newEnd = end; let newValue = ""; let l = matches.length; for (let i = 0; i < l; i++) { const [matchStart, matchEnd] = matches[i]; const lengthDiff = newLen - matchEnd + matchStart; const move = (pos) => matchStart > pos ? 0 : pos >= matchEnd ? lengthDiff : lengthDiff < 0 && pos > matchStart + newLen ? newLen + matchStart - pos : 0; newEnd += move(end); newStart += move(start); newValue += i ? value.slice(matches[i - 1][1], matchStart) + str : str; } insertText(editor, newValue, matches[0][0], matches[l - 1][1], newStart, newEnd); } }), [] ); }; const shortcut = ` (Alt+${isMac ? "Cmd+" : ""}`; const template = /* @__PURE__ */ createTemplate( `<div class=prism-search-container style=display:flex;align-items:flex-start;justify-content:flex-end><div dir=ltr class=prism-search><button type=button aria-expanded=false title="Toggle Replace" class=pce-expand></button><div spellcheck=false><div><div class="pce-input pce-find"><input autocorrect=off autocapitalize=off placeholder=Find aria-label=Find><button type=button class=prev-match title="Previous Match (Shift+Enter)"></button><button type=button class=next-match title="Next Match (Enter)"></button><div class=search-error></div></div><button type=button class=pce-close title="Close (Esc)"></button></div><div class="pce-input pce-replace"><input autocorrect=off autocapitalize=off placeholder=Replace aria-label=Replace><button type=button title=(Enter)>Replace</button><button type=button title=(${isMac ? "Cmd" : "Ctrl+Alt"}+Enter)>All</button></div><div class=pce-options><div class=pce-match-count>0<span> of </span>0</div><button type=button aria-pressed=false class=pce-regex title="RegExp Search${shortcut}R)"><span aria-hidden=true></span></button><button type=button aria-pressed=false title="Preserve Case${shortcut}P)"><span aria-hidden=true>Aa</span></button><button type=button aria-pressed=false class=pce-whole title="Match Whole Word${shortcut}W)"><span aria-hidden=true>ab</span></button><button type=button aria-pressed=false class=pce-in-selection title="Find in Selection${shortcut}L)">` ); const toggleAttr = (el, name) => el.setAttribute(name, el.getAttribute(name) == "false"); const useSearchWidget = (editor) => { const replaceAPI = useEditorReplace(editor, "pce-matches"); useEffect(() => { let prevLength; let useRegExp; let matchCase; let wholeWord; let searchSelection; let isOpen; let currentSelection; let prevUserSelection; let prevMargin; let selectNext = false; let marginTop; const searchContainer = template(); const search = searchContainer.firstChild; const [toggle, div] = search.children; const rows = div.children; const [findContainer, closeEl] = rows[0].children; const [findInput, prevEl, nextEl, errorEl] = findContainer.children; const [replaceInput, replaceEl, replaceAllEl] = rows[1].children; const [matchCount, useRegExpEl, matchCaseEl, wholeWordEl, inSelectionEl] = rows[2].children; const [current, , total] = matchCount.childNodes; const { textarea, container, getSelection, wrapper } = editor; const startSearch = (selectMatch) => { if (selectMatch && !isWebKit) textarea.setSelectionRange(...prevUserSelection); const error = replaceAPI.search( findInput.value, matchCase, wholeWord, useRegExp, searchSelection ); const index = error ? -1 : selectNext ? replaceAPI.next() : replaceAPI.closest(); updateNode(current, index + 1); updateNode(total, replaceAPI.matches.length); findContainer.classList.toggle("pce-error", !!error); if (error) errorEl.textContent = error; else if (selectMatch || selectNext) replaceAPI.selectMatch(index, prevMargin); }; const keydown = (e) => { if (e.keyCode >> 1 == 35 && getModifierCode(e) == (isMac ? 4 : 2)) { preventDefault(e); open(); let [start, end] = getSelection(); let value = editor.value; let word = value.slice(start, end) || /[_\p{N}\p{L}]*$/u.exec(getLineBefore(value, start))[0] + /^[_\p{N}\p{L}]*/u.exec(value.slice(start))[0]; if (/^$|\n/.test(word)) startSearch(); else { if (useRegExp) word = regexEscape(word); doc.execCommand("insertText", false, word); findInput.select(); } } }; const cleanups = [ addListener2(textarea, "keydown", keydown), addListener2(textarea, "beforeinput", () => { if (isOpen && searchSelection) currentSelection = getSelection(); }), editor.on("selectionChange", (selection) => { if (isOpen && editor.focused) prevUserSelection = selection; }), editor.on("update", () => { if (!isOpen) return; if (searchSelection && currentSelection) { const diff = prevLength - (prevLength = editor.value.length); const end = currentSelection[1]; if (end <= searchSelection[1]) { searchSelection[1] -= diff; if (end <= searchSelection[0] - +(diff < 0)) searchSelection[0] -= diff; } } startSearch(); }) ]; const open = (focusInput = true) => { if (!isOpen) { isOpen = true; if (marginTop == null) prevMargin = marginTop = getStyleValue(wrapper, "marginTop"); prevUserSelection = getSelection(); addOverlay(editor, searchContainer); updateMargin(); resize(); observer?.observe(container); } if (focusInput) findInput.select(); }; const close = (focusTextarea = true) => { if (isOpen) { isOpen = false; observer?.disconnect(); replaceAPI.stopSearch(); searchContainer.remove(); updateMargin(); if (focusTextarea) textarea.focus(); } }; const move = (next) => { if (replaceAPI.matches[0]) { const index = replaceAPI[next ? "next" : "prev"](); replaceAPI.selectMatch(index, prevMargin); updateNode(current, index + 1); } }; const updateMargin = () => { const newMargin = isOpen ? getStyleValue(search, "top") + getStyleValue(search, "height") : marginTop; const newScroll = container.scrollTop + newMargin - prevMargin; wrapper.style.marginTop = isOpen ? newMargin + "px" : ""; container.scrollTop = newScroll; prevMargin = newMargin; }; const resize = () => div.style.setProperty( "--search-width", `min(${container.clientWidth - 2}px - 2.4em - var(--padding-left),20em)` ); const observer = window.ResizeObserver && new ResizeObserver(resize); const replace = () => { selectNext = true; const index = replaceAPI.replace(replaceInput.value); if (index != null) { updateNode(current, index + 1); replaceAPI.selectMatch(index, prevMargin); } selectNext = false; }; const replaceAll = () => { replaceAPI.replaceAll(replaceInput.value); }; const keyCodeButtonMap = { 80: matchCaseEl, 87: wholeWordEl, 82: useRegExpEl, 76: inSelectionEl }; const elementHandlerMap = /* @__PURE__ */ new Map([ [nextEl, () => move(true)], [prevEl, move], [closeEl, close], [replaceEl, replace], [replaceAllEl, replaceAll], [ toggle, () => { toggleAttr(toggle, "aria-expanded"); updateMargin(); } ], [matchCaseEl, () => matchCase = !matchCase], [useRegExpEl, () => useRegExp = !useRegExp], [wholeWordEl, () => wholeWord = !wholeWord], [ inSelectionEl, () => { const value = editor.value; if (searchSelection) searchSelection = void 0; else { searchSelection = getSelection().slice(0, 2); if (numLines(value, ...searchSelection) > 1) { searchSelection = [ getLineStart(value, searchSelection[0]), getLineEnd(value, searchSelection[1]) ]; } } prevLength = value.length; } ] ]); addListener(searchContainer, "click", (e) => { const target = e.target; const remove = editor.on("update", () => target.focus()); elementHandlerMap.get(target)?.(); if (target.matches(".pce-options>button")) { toggleAttr(target, "aria-pressed"); startSearch(true); } remove(); }); addListener(findInput, "input", () => isOpen && startSearch(true)); addListener(searchContainer, "keydown", (e) => { const shortcut2 = getModifierCode(e); const target = e.target; const keyCode = e.keyCode; const isFind = target == findInput; if (shortcut2 == (isMac ? 5 : 1)) { if (keyCodeButtonMap[keyCode]) { preventDefault(e); keyCodeButtonMap[keyCode].click(); } } else if (keyCode == 13 && target.tagName == "INPUT") { preventDefault(e); if (!shortcut2) isFind ? move(true) : replaceEl.click(); else if (shortcut2 == 8 && isFind) move(); else if (shortcut2 == (isMac ? 4 : 3) && !isFind) replaceAllEl.click(); target.focus(); } else if (!shortcut2 && keyCode == 27) close(); else keydown(e); }); editor.extensions.searchWidget = { open(focusInput) { open(focusInput); startSearch(); }, close }; return () => { delete editor.extensions.searchWidget; cleanups.forEach((c) => c()); close(false); }; }, []); }; const useHighlightSelectionMatches = (editor, caseSensitive, minLength = 1, maxLength = 200) => { const searchAPI = useEditorSearch(editor, "selection-matches", -1); useLayoutEffect(() => { return editor.on("selectionChange", ([start, end], value) => { value = editor.focused ? value.slice(start, end) : ""; const pos = start + value.search(/\S/); const l = (value = value.trim()).length; searchAPI.search( minLength > l || l > maxLength ? "" : value, caseSensitive, false, false, void 0, (mStart, mEnd) => mStart > pos || mEnd <= pos ); }); }, [caseSensitive, minLength, maxLength]); }; const useHighlightCurrentWord = (editor, filter, includeHyphens) => { const searchAPI = useEditorSearch(editor, "word-matches", -1); useLayoutEffect(() => { let noHighlight = false; let cleanup1 = editor.on("update", () => noHighlight = true); let cleanup2 = editor.on("selectionChange", ([start, end], value) => { if (start < end || !editor.focused || noHighlight) searchAPI.search(""); else { let group = `[_$\\p{L}\\d${includeHyphens && includeHyphens(start) ? "-" : ""}]`; let before = value.slice(0, start).match(RegExp(group + "*$", "u")); let index = before.index; let word = before[0] + value.slice(start).match(RegExp("^" + group + "*", "u"))[0]; searchAPI.search( /^-*(\d|$)/.test(word) || filter && !filter(index, index + word.length) ? "" : word, true, true, false, void 0, filter, RegExp(group + "{2}", "u") ); } noHighlight = false; }); return () => { cleanup1(); cleanup2(); }; }, [filter, includeHyphens]); }; const useShowInvisibles = (editor, alwaysShow) => { const show = useStableRef([alwaysShow]); const searchAPI = useEditorSearch(editor, "pce-invisibles"); show[0] = alwaysShow; useLayoutEffect(() => { let prev; const matches = searchAPI.matches; const container = searchAPI.container; const nodes = container.children; const tabs = []; const update = () => { const value = editor.value; const [start, end] = editor.getSelection(); if (!show[0] || prev != (prev = value)) { searchAPI.search(" | ", true, false, true, show[0] ? void 0 : [start, end]); for (let i = 0, l = matches.length; i < l; i++) { if (value[matches[i][0]] == " " == !tabs[i]) { nodes[i].className = (tabs[i] = !tabs[i]) ? "pce-tab" : ""; } } } }; if (editor.value) update(); return editor.on("selectionChange", update); }, []); }; export { useSearchWidget as a, useHighlightSelectionMatches as b, useHighlightCurrentWord as c, useShowInvisibles as d, useEditorReplace as u }; //# sourceMappingURL=invisibles-C1HUPmS1.js.map