prism-react-editor
Version:
Lightweight, extensible code editor component for React apps
366 lines (365 loc) • 15.9 kB
JavaScript
"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