// get textarea element
var bodyElem = document.querySelector('textarea[name="body"]');
// --- Paste handler: convert selection + pasted URL into Markdown link ---
function markdownAutoFormat(event) {
const clipboardData = event.clipboardData || window.clipboardData;
const pastedData = clipboardData.getData('text');
const start = bodyElem.selectionStart;
const end = bodyElem.selectionEnd;
if (start !== end) {
event.preventDefault();
const selectedText = bodyElem.value.substring(start, end);
const before = bodyElem.value.substring(0, start);
const after = bodyElem.value.substring(end);
const markdownLink = `[${selectedText}](${pastedData})`;
bodyElem.value = before + markdownLink + after;
const newCursorPosition = before.length + markdownLink.length;
bodyElem.setSelectionRange(newCursorPosition, newCursorPosition);
}
}
// --- Keyboard shortcuts for formatting ---
function handleKeyDown(event) {
const start = bodyElem.selectionStart;
const end = bodyElem.selectionEnd;
const selectedText = bodyElem.value.substring(start, end);
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const ctrlKey = isMac ? event.metaKey : event.ctrlKey;
// --- Inline code: Ctrl/Cmd+Shift+C (Mac-safe) ---
if (ctrlKey && event.shiftKey && event.key.toLowerCase() === 'c') {
event.preventDefault();
wrapSelection('`', '`');
return;
}
// --- Bold: Ctrl/Cmd+B ---
if (ctrlKey && event.key.toLowerCase() === 'b') {
event.preventDefault();
wrapSelection('**', '**');
return;
}
// --- Italics: Ctrl/Cmd+I ---
if (ctrlKey && event.key.toLowerCase() === 'i') {
event.preventDefault();
wrapSelection('*', '*');
return;
}
// --- Strikethrough: Ctrl+Shift+S ---
if (ctrlKey && event.shiftKey && event.key.toLowerCase() === 's') {
event.preventDefault();
wrapSelection('~~', '~~');
return;
}
// --- Headings: Ctrl/Cmd+1..6 ---
if (ctrlKey && !event.shiftKey) {
const headingLevel = parseInt(event.key);
if (headingLevel >= 1 && headingLevel <= 6) {
event.preventDefault();
insertAtLineStart('#'.repeat(headingLevel) + ' ');
return;
}
}
// --- Insert link: Ctrl/Cmd+K ---
if (ctrlKey && event.key.toLowerCase() === 'k') {
event.preventDefault();
const url = prompt('Enter URL:');
if (url) wrapSelection('[', `](${url})`);
return;
}
// --- Insert image: Ctrl/Cmd+Shift+L ---
if (ctrlKey && event.shiftKey && event.key.toLowerCase() === 'l') {
event.preventDefault();
const url = prompt('Enter image URL:');
if (url) wrapSelection('`);
return;
}
// --- Insert footnote: Ctrl/Cmd+Shift+F ---
if (ctrlKey && event.shiftKey && event.key.toLowerCase() === 'f') {
event.preventDefault();
const footnoteId = prompt('Enter footnote label or number:');
if (footnoteId) wrapSelection(`[^${footnoteId}]`, '');
return;
}
// --- Insert table template: Ctrl/Cmd+Shift+T ---
if (ctrlKey && event.shiftKey && event.key.toLowerCase() === 't') {
event.preventDefault();
const tableTemplate = '| Header 1 | Header 2 |\n| --- | --- |\n| Cell 1 | Cell 2 |\n';
insertAtCursor(tableTemplate);
return;
}
// --- Blockquote / list indentation ---
if (event.key === 'Tab') {
event.preventDefault();
const lines = getSelectedLines();
if (event.shiftKey) {
// remove >
replaceLines(lines, line => line.replace(/^>\s?/, ''));
// remove list indentation
replaceLines(lines, line => line.replace(/^ {1,2}/, ''));
} else {
// add >
replaceLines(lines, line => '> ' + line);
// add list indentation
replaceLines(lines, line => ' ' + line);
}
return;
}
}
// --- Helper to wrap selection with prefix/suffix ---
function wrapSelection(prefix, suffix) {
const start = bodyElem.selectionStart;
const end = bodyElem.selectionEnd;
const selectedText = bodyElem.value.substring(start, end);
const before = bodyElem.value.substring(0, start);
const after = bodyElem.value.substring(end);
bodyElem.value = before + prefix + selectedText + suffix + after;
bodyElem.setSelectionRange(
start + prefix.length,
end + prefix.length
);
}
// --- Insert text at cursor (no selection) ---
function insertAtCursor(text) {
const start = bodyElem.selectionStart;
const end = bodyElem.selectionEnd;
const before = bodyElem.value.substring(0, start);
const after = bodyElem.value.substring(end);
bodyElem.value = before + text + after;
const cursorPos = start + text.length;
bodyElem.setSelectionRange(cursorPos, cursorPos);
}
// --- Insert at start of selected lines ---
function insertAtLineStart(prefix) {
const lines = getSelectedLines();
replaceLines(lines, line => prefix + line);
}
// --- Helper to get selected lines ---
function getSelectedLines() {
const start = bodyElem.selectionStart;
const end = bodyElem.selectionEnd;
const value = bodyElem.value;
const before = value.substring(0, start);
const after = value.substring(end);
const startLineIndex = before.lastIndexOf('\n') + 1;
const endLineIndex = end + after.indexOf('\n');
const selectedText = value.substring(startLineIndex, endLineIndex === -1 ? value.length : endLineIndex);
return { startLineIndex, endLineIndex: endLineIndex === -1 ? value.length : endLineIndex, text: selectedText };
}
// --- Helper to replace each line in a selection ---
function replaceLines(linesObj, fn) {
const { startLineIndex, endLineIndex, text } = linesObj;
const before = bodyElem.value.substring(0, startLineIndex);
const after = bodyElem.value.substring(endLineIndex);
const newText = text.split('\n').map(fn).join('\n');
bodyElem.value = before + newText + after;
bodyElem.setSelectionRange(startLineIndex, startLineIndex + newText.length);
}
// --- Event listeners for Undo/Redo ---
bodyElem.addEventListener('paste', markdownAutoFormat);
bodyElem.addEventListener('keydown', handleKeyDown);
function wrapSelection(prefix, suffix) {
const start = bodyElem.selectionStart;
const end = bodyElem.selectionEnd;
const selectedText = bodyElem.value.substring(start, end);
const wrapped = prefix + selectedText + suffix;
// Use execCommand to keep undo stack
bodyElem.focus();
bodyElem.setSelectionRange(start, end);
if (document.queryCommandSupported('insertText')) {
document.execCommand('insertText', false, wrapped);
} else {
// fallback
const before = bodyElem.value.substring(0, start);
const after = bodyElem.value.substring(end);
bodyElem.value = before + wrapped + after;
bodyElem.setSelectionRange(start + prefix.length, end + prefix.length);
}
}