const { addPluginToPluginManager, getCharacterRange, moveCaretTo, getCurrentLine, } = require("../utils"); const { createCodeBlock, isCodeBlock, state, newlineToBR } = require("./utils"); const { addCodeBlockToolbar, refreshHighlighting } = require("./toolbar"); const TAB = `\u00A0\u00A0`; const TAB_SEQUENCE = ["\u00A0", "\u0020"]; const TAB_LENGTH = 2; const EMPTY_LINE = "
/gm, "")
.replace(/<\/p>|
/gm, "\n");
return;
}
break;
// TextPattern plugin by default adds a new line for the replacement patterns,
// but we don't want to add a new line so we override it.
case "mceInsertNewLine":
if (isCreatingCodeBlock) {
e.preventDefault();
isCreatingCodeBlock = false;
return;
}
break;
}
}
// TinyMCE doesn't handle tab key events by default.
// This makes for a poor code block experience so
// we have to handle all the logic manually.
function onKeyDown(e) {
const node = state.activeBlock;
if (!node) return;
if (e.code === "Tab") {
e.preventDefault();
editor.undoManager.transact(() => {
handleTab(e, node);
});
}
}
function handleTab(e, node) {
const characterRange = getCharacterRange(node);
if (!characterRange) return;
// Shift + Tab = Deindent on all major platforms
const isDeindent = e.shiftKey;
const text = node.innerText;
const isTextSelected = characterRange.start !== characterRange.end;
if (isTextSelected) {
// we need handle multiline tabbing as well
// so we split the selected code into lines
// and indent each line seperately.
let [beforeSelection, selection, afterSelection] = [
text.substring(0, characterRange.start),
text.substring(characterRange.start, characterRange.end),
text.substring(characterRange.end),
];
let content = beforeSelection;
const selectedLines = selection.split("\n");
for (var i = 0; i < selectedLines.length; ++i) {
const line = selectedLines[i];
selectedLines[i] = isDeindent
? deindent(line, TAB_LENGTH)
: `${TAB}${line}`;
}
content += selectedLines.join("\n");
content += afterSelection;
node.innerHTML = newlineToBR(content);
const endIndex = isDeindent
? characterRange.end - TAB_LENGTH * selectedLines.length
: characterRange.end + TAB_LENGTH * selectedLines.length;
moveCaretTo(node, characterRange.start, endIndex);
} else {
if (isDeindent) {
const currentLine = getCurrentLine(node);
node.innerHTML = newlineToBR(
node.innerText.replace(currentLine, deindent(currentLine, TAB_LENGTH))
);
moveCaretTo(node, characterRange.start - TAB_LENGTH);
} else {
editor.insertContent(TAB);
}
}
}
function onKeyUp(e) {
if (e.code === "Enter") {
const node = state.activeBlock;
if (!node) return;
editor.undoManager.transact(() => {
handleEnter(node);
});
}
debounce(() => {
refreshHighlighting(editor);
}, 500)();
}
function handleEnter(node) {
const currentLine = getCurrentLine(node);
const indent =
(currentLine.length - currentLine.trimStart().length) / TAB_LENGTH;
setTimeout(() => editor.insertContent(TAB.repeat(indent)), 0);
}
editor.on("BeforeExecCommand", onBeforeExecCommand);
editor.on("BeforeSetContent", onBeforeSetContent);
editor.on("keydown", onKeyDown);
editor.on("keyup", onKeyUp);
return function() {
editor.off("BeforeExecCommand", onBeforeExecCommand);
editor.off("BeforeSetContent", onBeforeSetContent);
editor.off("keydown", onKeyDown);
editor.off("keyup", onKeyUp);
};
};
function deindent(line, tabLength) {
for (let i = 0; i < tabLength; ++i) {
const char = line[i];
if (!TAB_SEQUENCE.some((c) => c === char)) return line;
}
return line.substring(2);
}
(function init() {
addPluginToPluginManager("codeblock", register);
})();
/**
* Call this function in paste_postprocess function in tinymce.init.
* This basically removes all internal formatting of code blocks and applies
* the necessary formatting for consistency.
*/
function processPastedContent(node) {
if (!node) return;
if (node.childNodes) {
for (let childNode of node.childNodes) {
if (childNode.tagName === "PRE") {
childNode.className = "hljs";
const code = childNode.textContent || childNode.innerText;
childNode.innerHTML = code;
}
}
}
}
module.exports = { processPastedContent };
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this,
args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}