UNPKG

prism-react-editor

Version:

Lightweight, extensible code editor component for React apps

366 lines (365 loc) 15.9 kB
"use client"; import { useEffect } from "react"; import { u as useStableRef, p as preventDefault, l as languageMap } from "../core-Dm5I6BkG.js"; import { a as addTextareaListener, b as addListener2, p as prevSelection, g as getModifierCode, i as isMac, c as getLanguage, d as insertText, e as getLines, f as getLineEnd, h as getLineBefore, j as getLineStart, r as regexEscape, s as setSelection } from "../local-Cq-4Fajb.js"; import { g as getStyleValue } from "../other-EdiSn7BB.js"; let ignoreTab = false; const addCommand = (cleanups, commands, key, command) => { commands[key] = command; cleanups.push(() => delete commands[key]); }; const mod = isMac ? 4 : 2; const setIgnoreTab = (newState) => ignoreTab = newState; const whitespaceEnd = (str) => str.search(/\S|$/); const useDefaultCommands = (editor, selfClosePairs = ['""', "''", "``", "()", "[]", "{}"], selfCloseRegex = /([^$\w'"`]["'`]|.[[({])[.,:;\])}>\s]|.[[({]`/s) => { const props = useStableRef([selfClosePairs, selfCloseRegex]); props[0] = selfClosePairs; props[1] = selfCloseRegex; useEffect(() => { let prevCopy; const { keyCommandMap, inputCommandMap, getSelection, container } = editor; const clipboard = navigator.clipboard; const getIndent = ({ insertSpaces = true, tabSize } = editor.props) => [insertSpaces ? " " : " ", insertSpaces ? tabSize || 2 : 1]; const scroll = () => !editor.props.readOnly && !editor.extensions.cursor?.scrollIntoView(); const selfClose = ([start, end], [open, close], value, wrapOnly) => (start < end || !wrapOnly && props[1].test((value[end - 1] || " ") + open + (value[end] || " "))) && !insertText(editor, open + value.slice(start, end) + close, null, null, start + 1, end + 1); const skipIfEqual = ([start, end], char, value) => start == end && value[end] == char && !setSelection(editor, start + 1); const insertLines = (old, newL, start, end, selectionStart, selectionEnd) => { let newLines = newL.join("\n"); if (newLines != old.join("\n")) { const last = old.length - 1; const lastLine = newL[last]; const oldLastLine = old[last]; const lastDiff = oldLastLine.length - lastLine.length; const firstDiff = newL[0].length - old[0].length; const firstInsersion = start + whitespaceEnd((firstDiff < 0 ? newL : old)[0]); const lastInsersion = end - oldLastLine.length + whitespaceEnd(lastDiff > 0 ? lastLine : oldLastLine); const offset = start - end + newLines.length + lastDiff; const newCursorStart = firstInsersion > selectionStart ? selectionStart : Math.max(firstInsersion, selectionStart + firstDiff); const newCursorEnd = selectionEnd + start - end + newLines.length; insertText( editor, newLines, start, end, newCursorStart, selectionEnd < lastInsersion ? newCursorEnd + lastDiff : Math.max(lastInsersion + offset, newCursorEnd) ); } }; const indent = (outdent, lines, start1, end1, start, end, indentChar, tabSize) => { insertLines( lines, lines.map( outdent ? (str) => str.slice(whitespaceEnd(str) ? tabSize - whitespaceEnd(str) % tabSize : 0) : (str) => str && indentChar.repeat(tabSize - whitespaceEnd(str) % tabSize) + str ), start1, end1, start, end ); }; const cleanUps = []; addCommand( cleanUps, inputCommandMap, "<", (_e, selection, value) => selfClose(selection, "<>", value, true) ); props[0].forEach(([open, close]) => { const isQuote = open == close; addCommand( cleanUps, inputCommandMap, open, (_e, selection, value) => (isQuote && skipIfEqual(selection, close, value) || selfClose(selection, open + close, value)) && scroll() ); if (!isQuote) addCommand( cleanUps, inputCommandMap, close, (_e, selection, value) => skipIfEqual(selection, close, value) && scroll() ); }); addCommand(cleanUps, inputCommandMap, ">", (e, selection, value) => { const closingTag = languageMap[getLanguage(editor)]?.autoCloseTags?.(selection, value, editor); if (closingTag) { insertText(editor, ">" + closingTag, null, null, selection[0] + 1); preventDefault(e); } }); addCommand(cleanUps, keyCommandMap, "Tab", (e, [start, end], value) => { if (ignoreTab || editor.props.readOnly || getModifierCode(e) & 6) return; const [indentChar, tabSize] = getIndent(); const shiftKey = e.shiftKey; const [lines, start1, end1] = getLines(value, start, end); if (start < end || shiftKey) { indent(shiftKey, lines, start1, end1, start, end, indentChar, tabSize); } else insertText(editor, indentChar.repeat(tabSize - (start - start1) % tabSize)); return scroll(); }); addCommand(cleanUps, keyCommandMap, "Enter", (e, [start, end, dir], value) => { const code = getModifierCode(e) & 7; if (!code || code == mod) { if (code) start = end = getLineEnd(value, dir > "f" ? end : start); const [indentChar, tabSize] = getIndent(); const selection = [start, end, dir]; const autoIndent = languageMap[getLanguage(editor, start)]?.autoIndent; const indenationCount = Math.floor(whitespaceEnd(getLineBefore(value, start)) / tabSize) * tabSize; const extraIndent = autoIndent?.[0]?.(selection, value, editor) ? tabSize : 0; const extraLine = autoIndent?.[1]?.(selection, value, editor); const newText = "\n" + indentChar.repeat(indenationCount + extraIndent) + (extraLine ? "\n" + indentChar.repeat(indenationCount) : ""); if (newText[1] || value[end]) { insertText(editor, newText, start, end, start + indenationCount + extraIndent + 1); return scroll(); } } }); addCommand(cleanUps, keyCommandMap, "Backspace", (_e, [start, end], value) => { if (start == end) { const line = getLineBefore(value, start); const tabSize = editor.props.tabSize || 2; const isPair = selfClosePairs.includes(value.slice(start - 1, start + 1)); const indenationCount = /[^ ]/.test(line) ? 0 : (line.length - 1) % tabSize + 1; if (isPair || indenationCount > 1) { insertText(editor, "", start - (isPair ? 1 : indenationCount), start + isPair); return scroll(); } } }); for (let i = 0; i < 2; i++) addCommand(cleanUps, keyCommandMap, i ? "ArrowDown" : "ArrowUp", (e, [start, end], value) => { const code = getModifierCode(e); if (code == 1) { const newStart = i ? start : getLineStart(value, start) - 1; const newEnd = i ? value.indexOf("\n", end) + 1 : end; if (newStart > -1 && newEnd > 0) { const [lines, start1, end1] = getLines(value, newStart, newEnd); const line = lines[i ? "pop" : "shift"](); const offset = (line.length + 1) * (i ? 1 : -1); lines[i ? "unshift" : "push"](line); insertText(editor, lines.join("\n"), start1, end1, start + offset, end + offset); } return scroll(); } else if (code == 9) { const [lines, start1, end1] = getLines(value, start, end); const str = lines.join("\n"); const offset = i ? str.length + 1 : 0; insertText(editor, str + "\n" + str, start1, end1, start + offset, end + offset); return scroll(); } else if (code == 2 && !isMac) { container.scrollBy(0, getStyleValue(container, "lineHeight") * (i ? 1 : -1)); return true; } }); cleanUps.push( addTextareaListener(editor, "keydown", (e) => { const code = getModifierCode(e); const keyCode = e.keyCode; const [start, end, dir] = getSelection(); if (code == mod && (keyCode == 221 || keyCode == 219)) { indent(keyCode == 219, ...getLines(editor.value, start, end), start, end, ...getIndent()); scroll(); preventDefault(e); } else if (code == (isMac ? 10 : 2) && keyCode == 77) { setIgnoreTab(!ignoreTab); preventDefault(e); } else if (keyCode == 191 && code == mod || keyCode == 65 && code == 9) { const value = editor.value; const isBlock = code == 9; const position = isBlock ? start : getLineStart(value, start); const language = languageMap[getLanguage(editor, position)] || {}; const { line, block } = language.getComments?.(editor, position, value) || language.comments || {}; const [lines, start1, end1] = getLines(value, start, end); const last = lines.length - 1; if (isBlock) { if (block) { const [open, close] = block; const text = value.slice(start, end); const pos = value.slice(0, start).search(regexEscape(open) + " ?$"); const matches = RegExp("^ ?" + regexEscape(close)).test(value.slice(end)); if (pos + 1 && matches) insertText( editor, text, pos, end + +(value[end] == " ") + close.length, pos, pos + end - start ); else insertText( editor, `${open} ${text} ${close}`, start, end, start + open.length + 1, end + open.length + 1 ); scroll(); preventDefault(e); } } else { if (line) { const escaped = regexEscape(line); const regex = RegExp(`^\\s*(${escaped} ?|$)`); const regex2 = RegExp(escaped + " ?"); const allWhiteSpace = !/\S/.test(value.slice(start1, end1)); const newLines = lines.map( lines.every((line2) => regex.test(line2)) && !allWhiteSpace ? (str) => str.replace(regex2, "") : (str) => allWhiteSpace || /\S/.test(str) ? str.replace(/^\s*/, `$&${line} `) : str ); insertLines(lines, newLines, start1, end1, start, end); scroll(); preventDefault(e); } else if (block) { const [open, close] = block; const insertionPoint = whitespaceEnd(lines[0]); const hasComment = lines[0].startsWith(open, insertionPoint) && lines[last].endsWith(close); const newLines = lines.slice(); newLines[0] = lines[0].replace( hasComment ? RegExp(regexEscape(open) + " ?") : /(?=\S)|$/, hasComment ? "" : open + " " ); let diff = newLines[0].length - lines[0].length; newLines[last] = hasComment ? newLines[last].replace(RegExp(`( ?${regexEscape(close)})?$`), "") : newLines[last] + " " + close; let newText = newLines.join("\n"); let firstInsersion = insertionPoint + start1; let newStart = firstInsersion > start ? start : Math.max(start + diff, firstInsersion); let newEnd = firstInsersion > end - (start != end) ? end : Math.min(Math.max(firstInsersion, end + diff), start1 + newText.length); insertText(editor, newText, start1, end1, newStart, Math.max(newStart, newEnd)); scroll(); preventDefault(e); } } } else if (code == 8 + mod && keyCode == 75) { const value = editor.value; const [lines, start1, end1] = getLines(value, start, end); const column = dir == "forward" ? end - end1 + lines.pop().length : start - start1; const newLineLen = getLines(value, end1 + 1)[0][0].length; insertText( editor, "", start1 - !!start1, end1 + !start1, start1 + Math.min(column, newLineLen) ); scroll(); preventDefault(e); } }), ...["copy", "cut", "paste"].map( (type) => addTextareaListener(editor, type, (e) => { const [start, end] = getSelection(); if (start == end && clipboard) { const [[line], start1, end1] = getLines(editor.value, start, end); if (type == "paste") { if (e.clipboardData.getData("text/plain") == prevCopy) { insertText(editor, prevCopy + "\n", start1, start1, start + prevCopy.length + 1); scroll(); preventDefault(e); } } else { clipboard.writeText(prevCopy = line); if (type == "cut") insertText(editor, "", start1, end1 + 1), scroll(); preventDefault(e); } } }) ) ); return () => { cleanUps.forEach((cleanUp) => cleanUp()); }; }, []); }; const useEditHistory = (editor, historyLimit = 999) => { const limit = useStableRef([historyLimit]); limit[0] = historyLimit; useEffect(() => { let sp = 0; let allowMerge; let isTyping = false; let prevInputType; let prevData; let isMerge; let prevTime; const getSelection = editor.getSelection; const extensions = editor.extensions; const textarea = editor.textarea; const stack = []; const update = (index) => { if (index >= limit[0]) { index--; stack.shift(); } stack.splice(sp = index, limit[0], [editor.value, getSelection(), getSelection()]); }; const setEditorState = (index) => { if (stack[index]) { textarea.value = stack[index][0]; textarea.setSelectionRange(...stack[index][index < sp ? 2 : 1]); editor.update(); extensions.cursor?.scrollIntoView(); sp = index; allowMerge = false; } }; const cleanUps = [ addListener2(textarea, "beforeinput", (e) => { let data = e.data; let inputType = e.inputType; let time = e.timeStamp; if (/history/.test(inputType)) { setEditorState(sp + (inputType[7] == "U" ? -1 : 1)); preventDefault(e); } else if (!(isMerge = allowMerge && (prevInputType == inputType || time - prevTime < 99 && inputType.slice(-4) == "Drop") && !prevSelection && (data != " " || prevData == data))) { stack[sp][2] = prevSelection || getSelection(); } isTyping = true; prevTime = time; prevData = data; prevInputType = inputType; }), addListener2(textarea, "input", () => update(sp + !isMerge)), addListener2(textarea, "keydown", (e) => { if (!editor.props.readOnly) { const code = getModifierCode(e); const keyCode = e.keyCode; const isUndo = code == mod && keyCode == 90; const isRedo = code == mod + 8 && keyCode == 90 || !isMac && code == mod && keyCode == 89; if (isUndo) { setEditorState(sp - 1); preventDefault(e); } else if (isRedo) { setEditorState(sp + 1); preventDefault(e); } } }), editor.on("selectionChange", () => { allowMerge = isTyping; isTyping = false; }) ]; extensions.history = { clear() { update(0); allowMerge = false; }, has: (offset) => sp + offset in stack, go(offset) { setEditorState(sp + offset); } }; update(0); return () => { cleanUps.forEach((cleanUp) => cleanUp()); delete extensions.history; }; }, [editor.props.value]); }; export { useDefaultCommands, useEditHistory }; //# sourceMappingURL=commands.js.map