<h1>A Markdown Editor in a Textarea</h1>
<main>
<div class="plain">
<textarea class="plain__editor" data-el="input"># test
This is a *test*
With a [Link](https://iamschulz.com)!</textarea>
<pre aria-hidden="true" class="plain__highlights"><code data-el="highlight" class="markdown"></code></pre>
</div>
<div class="text">
<div data-el="output"></div>
</div>
</main>
main {
display: flex;
}
.plain,
.text {
flex: 1 0 20rem;
position: relative;
}
[data-el="input"] {
border-width: 1px;
color: black;
caret-color: black;
white-space: break-spaces;
word-break: break-word;
resize: vertical;
}
[data-el="input"][data-initialized="true"] {
color: transparent;
resize: none;
overflow: hidden;
}
[data-el="highlight"] {
font-family: inherit;
line-height: inherit;
font-size: inherit;
margin: 0;
padding: 0;
white-space: break-spaces;
word-break: break-word;
}
.plain__highlights {
position: absolute;
top: 0;
left: 0;
width: 90%;
border: 1px solid transparent;
pointer-events: none;
}
.plain__editor, .plain__highlights {
width: 90%;
font-size: 1.1rem;
font-family: monospace;
margin: 0;
padding: 0.7rem 1.4ch;
line-height: 1.313;
}
const inputEl = document.querySelector('[data-el="input"]');
const highlightEl = document.querySelector('[data-el="highlight"]');
const outputEl = document.querySelector('[data-el="output"]');
const converter = new showdown.Converter({
metadata: true,
parseImgDimensions: true,
strikethrough: true,
tables: true,
ghCodeBlocks: true,
smoothLivePreview: true,
simpleLineBreaks: true,
emoji: true,
});
const resizeTextarea = (textArea) => {
if (!textArea) {
return;
}
window.requestAnimationFrame(() => {
textArea.style.height = 0;
if (textArea.scrollHeight > 0) {
textArea.style.height = `${textArea.scrollHeight + 2}px`;
}
});
};
const highlight = () => {
window.requestAnimationFrame(() => {
const highlighted = hljs.highlight(
"markdown",
inputEl.value
).value;
highlightEl.innerHTML = highlighted;
});
};
const updateReadonly = () => {
window.requestAnimationFrame(() => {
const htmlContent = converter.makeHtml(inputEl.value);
outputEl.innerHTML = htmlContent;
});
};
const init = () => {
inputEl.addEventListener("input", () => {
resizeTextarea(inputEl);
highlight();
updateReadonly();
});
inputEl.setAttribute('data-initialized', true);
}
document.addEventListener("DOMContentLoaded", () => {
init();
resizeTextarea(inputEl);
highlight();
updateReadonly();
})