当我正在开发一个需要源代码编辑器组件的项目时,我真的很想有一种方法可以让编辑器突出显示输入的语法。 有像这样的项目,比如 CodeMirror、Ace 和 Monaco,但它们都是重量级、功能齐全的编辑器,而不仅仅是我想要的带有语法高亮的 textareas
。
经过一番努力,我最终做出了一些能够完成这项工作的东西,并想分享我是如何做到的,因为它涉及将一个流行的语法高亮库与 HTML 的编辑功能集成在一起,以及一些需要考虑的有趣边缘情况。

在我们深入研究之前,先试用一下吧!
在收到建议后,我还将其作为 GitHub 上的自定义元素 发布,因此您可以在网页中快速使用该组件作为单个 <code-input>
元素。
问题
首先,我尝试在 div 上使用 contenteditable
属性。 我在 div 中输入了一些源代码,并通过 Prism.js(一个流行的语法高亮器)在 oninput
上通过 JavaScript 运行它。 看起来是一个不错的主意,对吧? 我们有一个可以在前端编辑的元素,而 Prism.js 将其语法样式应用于在元素中输入的内容。
Chris 在 此视频 中介绍了如何使用 Prism.js。
但这行不通。 每次元素中的内容发生变化时,DOM 都会被操作,用户的游标会被推回代码的开头,这意味着源代码会反向显示,最后一个字符在开头,第一个字符在结尾。

接下来,我尝试使用 <textarea>
,但这也不行,因为 textareas 只能包含纯文本。 换句话说,我们无法对输入的内容进行样式设置。 textarea
似乎是唯一一种在没有意外错误的情况下编辑文本的方法——它只是不允许 Prism.js 发挥作用。
当源代码包含在典型的 <pre><code>
标签组合中时,Prism.js 的效果会好得多——它只是缺少方程式的可编辑部分。
因此,它们似乎都不能独立工作。 但我想,为什么不两者兼而有之呢?
解决方案
我在页面中添加了语法高亮的 <pre><code>
和 一个 textarea
,并使用 JavaScript 函数使 <pre><code>
的 innerText
内容在 oninput
时发生更改。 我还在 <pre><code>
结果中添加了一个 aria-hidden
属性,以便屏幕阅读器只读取 <textarea>
中输入的内容,而不是两次大声朗读。
<textarea id="editing" oninput="update(this.value);"></textarea>
<pre id="highlighting" aria-hidden="true">
<code class="language-html" id="highlighting-content"></code>
</pre>
function update(text) {
let result_element = document.querySelector("#highlighting-content");
// Update code
result_element.innerText = text;
// Syntax Highlight
Prism.highlightElement(result_element);
}

现在,当编辑 textarea
时——比如,键盘上的一个按下的键弹起来——语法高亮的代码会发生变化。 我们会遇到一些错误,但我想首先关注让它看起来像你正在直接编辑语法高亮的元素,而不是一个单独的 textarea
。
让它“感觉”像一个代码编辑器
我们的想法是将这些元素在视觉上合并在一起,以便看起来我们正在与一个元素进行交互,而实际上是两个元素在起作用。 我们可以添加一些 CSS,基本上允许 <textarea>
和 <pre><code>
元素的大小和间距保持一致。
#editing, #highlighting {
/* Both elements need the same text and space styling so they are directly on top of each other */
margin: 10px;
padding: 10px;
border: 0;
width: calc(100% - 32px);
height: 150px;
}
#editing, #highlighting, #highlighting * {
/* Also add text styles to highlighting tokens */
font-size: 15pt;
font-family: monospace;
line-height: 20pt;
}
然后我们想要将它们直接定位在彼此的顶部
#editing, #highlighting {
position: absolute;
top: 0;
left: 0;
}
从那里,z-index
允许 textarea
堆叠在突出显示的结果前面
/* Move the textarea in front of the result */
#editing {
z-index: 1;
}
#highlighting {
z-index: 0;
}
如果我们到此为止,我们会看到第一个错误。 它看起来不像 Prism.js 正在突出显示语法,但那是因为 textarea
正在遮盖结果。

我们可以用 CSS 修复它! 我们将使 <textarea>
完全透明,除了光标(游标)
/* Make textarea almost completely transparent */
#editing {
color: transparent;
background: transparent;
caret-color: white; /* Or choose your favorite color */
}
啊,好多了!

更多修复!
完成这些之后,我尝试使用编辑器一段时间,发现了一些还需要修复的地方。 好消息是,所有问题都非常容易使用 JavaScript、CSS 甚至 HTML 来修复。
删除原生拼写检查
我们正在制作一个代码编辑器,代码中有很多单词和属性,浏览器的原生拼写检查器会认为是拼写错误。

拼写检查不是一件坏事;它只是在这种情况下没有用。 是因为拼写错误而标记为错误,还是因为代码无效? 很难说。 要修复此问题,我们只需要将 <textarea>
上的 spellcheck
属性设置为 false
<textarea id="editing" spellcheck="false" ...>
处理换行符
事实证明,innerText
不支持换行符 (\n
)。

update
函数需要编辑。 我们可以使用 innerHTML
,而不是使用 innerText
,将左括号字符 (<
) 替换为 <
,并将 & 字符 (&
) 替换为 &
以阻止 HTML 转义被评估。 这将阻止创建新的 HTML 标签,允许显示实际的源代码,而不是浏览器尝试呈现代码。
result_element.innerHTML = text.replace(new RegExp("&", "g"), "&").replace(new RegExp("<", "g"), "<"); /* Global RegExp */

滚动和调整大小

还有另一件事:突出显示的代码在编辑时无法滚动。 当 textarea
滚动时,突出显示的代码不会随其滚动。
首先,让我们确保 textarea
和结果都支持滚动
/* Can be scrolled */
#editing, #highlighting {
overflow: auto;
white-space: nowrap; /* Allows textarea to scroll horizontally */
}
然后,为了确保结果与 textarea
一起滚动,我们将按照以下方式更新 HTML 和 JavaScript
<textarea id="editing" oninput="update(this.value); sync_scroll(this);" onscroll="sync_scroll(this);"></textarea>
function sync_scroll(element) {
/* Scroll result to scroll coords of event - sync with textarea */
let result_element = document.querySelector("#highlighting");
// Get and set x and y
result_element.scrollTop = element.scrollTop;
result_element.scrollLeft = element.scrollLeft;
}
某些浏览器还允许调整 textarea
的大小,但这意味着 textarea
和结果的大小可能不同。 CSS 可以解决这个问题吗? 当然可以。 我们只需禁用调整大小
/* No resize on textarea */
#editing {
resize: none;
}
最后的换行符

感谢 此评论 指出了此错误。
现在滚动几乎总是同步的,但仍然存在**一个它仍然不起作用的情况**。当用户创建新行时,光标和文本区域的文本在任何文本被输入到新行之前**暂时处于错误的位置**。这是因为**<pre><code>
块出于美观原因忽略空行结尾。** 因为这是用于功能性代码输入,而不是显示的代码片段,所以需要显示空行结尾。这是通过**为最后一行提供内容以使其不再为空**来完成的,在update
函数中添加了几行 JavaScript。我使用了空格字符,因为它**对用户不可见**。
function update(text) {
let result_element = document.querySelector("#highlighting-content");
// Handle final newlines (see article)
if(text[text.length-1] == "\n") { // If the last character is a newline character
text += " "; // Add a placeholder space character to the final line
}
// Update code
result_element.innerHTML = text.replace(new RegExp("&", "g"), "&").replace(new RegExp("<", "g"), "<"); /* Global RegExp */
// Syntax Highlight
Prism.highlightElement(result_element);
}
缩进行
调整起来比较棘手的一件事是如何处理结果中的行缩进。编辑器目前的设置方式是,使用空格缩进行可以正常工作。但是,如果您更喜欢使用制表符而不是空格,您可能已经注意到它们没有按预期工作。
可以使用 JavaScript 使 Tab 键正常工作。我在函数中添加了注释以清楚说明正在发生的事情。
<textarea ... onkeydown="check_tab(this, event);"></textarea>
function check_tab(element, event) {
let code = element.value;
if(event.key == "Tab") {
/* Tab key pressed */
event.preventDefault(); // stop normal
let before_tab = code.slice(0, element.selectionStart); // text before tab
let after_tab = code.slice(element.selectionEnd, element.value.length); // text after tab
let cursor_pos = element.selectionEnd + 1; // where cursor moves after tab - moving forward by 1 char to after tab
element.value = before_tab + "\t" + after_tab; // add tab char
// move cursor
element.selectionStart = cursor_pos;
element.selectionEnd = cursor_pos;
update(element.value); // Update text to include indent
}
}
为了确保**Tab
字符在<textarea>
和语法高亮代码块中大小相同**,请编辑CSS
以包含 tab-size
属性
#editing, #highlighting, #highlighting * {
/* Also add text styles to highlighing tokens */
[...]
tab-size: 2;
}
最终结果
不算太疯狂,对吧?我们只有<textarea>
、<pre>
和<code>
元素在 HTML 中,几行 CSS 将它们堆叠在一起,以及一个语法高亮库来格式化输入的内容。我最喜欢的是,我们正在使用普通的语义化 HTML 元素,利用原生属性来获得我们想要的行为,依靠 CSS 来创造我们只与一个元素交互的错觉,然后借助 JavaScript 来解决一些边缘情况。
虽然我使用了 Prism.js 进行语法高亮,但这种技术可以与其他工具配合使用。如果你愿意,它甚至可以与你自己创建的语法高亮程序一起使用。我希望这能派上用场,并在很多地方使用,无论是 CMS 的 WYSIWYG 编辑器,还是需要输入源代码的表单,例如前端工作申请或问卷调查。毕竟它是一个<textarea>
,所以它可以在任何表单中使用——你甚至可以添加一个placeholder
,如果你需要的话!
虽然它几乎完美地镜像了在具有内置语法高亮功能的代码编辑器中键入代码时的视觉效果,但您的版本仍然存在一些缺陷,例如在文本区域中键入
<
、>
或&
时,它分别显示<
、>
和&
而不是实际代码。此外,您能否使用input
事件而不是keyup
事件?使用input
事件将立即更新高亮显示,而keyup
会在键入和显示我键入的字符之间出现相当恼人的延迟。这是一个很好的观点。您可以通过编辑
update
函数中用于替换字符的 JavaScript 代码行来停止像&
这样的 HTML 转义,如以下所示。感谢您提醒我
input
事件。我将在 CodePen 演示中添加这两个更改。非常棒的解决方案,我喜欢缩进解决方法
太棒了!
我使用 CodeMirror 做过同样的事情,现在看来它太复杂了!
这绝对应该是一个像
<code-editor>
这样的 Web 组件。感谢您的评论;您启发我创建了一个
<code-input>
自定义元素!我已经在这里创建了一个 CodePen Pen,请随意使用此代码。如果您导入 JavaScript 和 Prism.js 的 CSS 文件,则可以在任何网站中使用它,因为它需要 Prism.js 才能工作。请注意,主要的“Funky” Prism.js CSS 主题(没有我在这里介绍的可编辑部分)是由 Lea Verou 创建的,而不是我,如代码中的注释所示。您可以通过将 CSS 代码的主题部分替换为 Prism.js 网站 上的任何主题来选择不同的主题。
我希望这对您有所帮助,并且希望您喜欢它!
我只想插一句,说这是一篇很棒的文章。这是一个很有意思的问题,您一步一步地详细介绍了如何解决它。
我之前也在 Google Code 中做过这件事,但不幸的是项目消失了。实际上,我们可以在这里使用任何 JavaScript 语法高亮程序,我认为基于正则表达式的语法高亮程序最适合这种情况,因为我们需要在每次击键时对整个代码运行语法高亮程序。
所以总的来说
从
<textarea>
获取选择之前的字符。
获取选择中的字符。
获取选择之后的字符。
到
<pre>
元素在每次击键时执行此操作!
这非常低效,您会在打开非常大的文档时感受到差异。
CodeMirror、Ace、Monaco 等之所以可以更快地工作,即使它们具有更大的源代码,是因为它们在流上工作。只有视图中的部分将被高亮显示,滚动条下方和上方的任何文本都将被忽略。
笔记
<textarea>
和<pre>
中的font-size
、font-family
、line-height
、text-indent
和letter-spacing
必须相同。<textarea>
和<pre>
中的width
、height
、padding
、margin
、border-width
、outline
和box-sizing
必须相同。太棒了!
从这个解决方案中获得灵感,我能够摆脱绝对定位和同步滚动。由于网格显示,您可以完美地叠加文本框和高亮显示的显示,文本框会扩展以占用 pre 元素的大小。
HTML
CSS
JS
我的高亮显示函数在 pre 元素中添加了...,有时叠加效果不好(没有跨度它可以正常工作,但我不再具有高亮显示)。您是否有解决我问题的想法?我认为它有时也会出现在您的解决方案中。
谢谢:)
我喜欢您如何使用 CSS Grid 来叠加元素。但是,我不完全理解您遇到的问题。我认为它可能与 我对 Thomas 的回复 有关,但如果不是,请详细说明问题的性质,我会尽快回复您。
尝试键入直到 div 溢出被激活,您将看到您忽略的第一个问题。
感谢您指出这一点。此错误可以通过将 CSS
white-space: nowrap
设置为#editing
和#highlighting
元素来轻松修复,以允许<textarea>
水平滚动。我已经将其添加到帖子中,并在下面附上代码片段谢谢!
是否可以在 pre-wrap 中使用它来禁用滚动条?
我无法弄清楚换行符部分,
result_element.innerHTML = text.replace(new RegExp("<", "g"), "<");
与换行符有什么关系?因为我使用的是
innerHTML
而不是innerText
(不支持换行),所以显示了换行符。由于它被放在一个<pre>
元素中,并且<pre>
元素会显示来自HTML的换行符,而不需要使用<br/>
,因此换行符会在<pre><code>
块中显示。**您提到的代码行对HTML标签进行了转义,因此浏览器将其视为文本而不是HTML,以解析为元素,与换行符无关。**我已经更新了文章,使这更加清晰。
看起来Notion应用程序使用div contentEditable,并且它工作正常...我想知道如何解决这个问题...
非常有趣的文章和技巧。
最初,我肯定不会考虑合并textarea和一个
<pre>
元素,而会苦苦挣扎于一个contenteditable :)我认为,可以使用
textContent
而不是innerText
来更优雅地解决换行符的处理问题,因为它支持换行符\n
当底部的换行符为空时,textarea和
<pre>
的对齐方式会错位一行。示例也存在此问题。如何解决呢?您可以使用JavaScript来解决此问题,正如我在文章中的解释。**感谢您指出这个错误!**
您好,
在将textarea中的代码提交到数据库后,它会遇到问题吗,或者不会?
它会在MySQL数据库中像我输入的那样输出吗?
例如
我尝试了图像嵌入,但它显示了图像错误,而不是显示嵌入图像代码
感谢您关于如何将此后端集成的问题;这似乎是我在撰写文章时没有充分考虑的问题。似乎您是在意外地解析HTML代码,而不是将其放入
value
中。**我已经在下面放入了链接,该链接演示了如何将此后端集成。**再次感谢您。
如果我从记事本中复制带有制表符的行,它就会中断
例如:“ 1 2 3”
尝试复制粘贴此内容,然后按Ctrl+A,您会看到编辑和突出显示不匹配。
如何解决它?
感谢您告知我这个错误。解决方案是使
<textarea>
和pre code
块的tab-size
属性相同。我已经更新了文章,以更详细地说明这一点。当选择处于活动状态时按下制表键,光标会向前跳转正好是所选数量。
此错误可以通过将
替换为
在
check_tab
函数中感谢您的分享!
我不知道是不是我太笨了,但我无法让任何Codepen示例正常工作 - 我可以在白色伪textarea中选择文本,并点击其中的链接,但我无法让光标出现以输入任何内容。我已经在macOS Big Sur上的Safari 15.1和Firefox 94.0.1以及iPadOS 15.1上的Safari上尝试过了。
Firefox 95.0
可见光标仍然停留在第一行。不幸的是,无法使用它。
要在Firefox中使其工作,请使用
在#editing和#highlighting上。
在Firefox中,将空白设置为pre-wrap对我来说不起作用,但pre可以。
在#editing、#highlighting和#highlighting code上
太棒了。我用它做了一个Python编辑器。ㅆ谢谢
要保存textarea并再次编辑它,文本代码不会被着色。您必须点击textarear并在键盘上输入内容,颜色才会生效。可以在没有键盘操作的情况下应用颜色吗?
看起来,虽然Prism.js会自动突出显示所有
pre code
元素,但使用highlight.js,您需要最初使用该元素运行update()
函数、hljs.highlightElement(
[pre code元素])
。您也可以使用hljs.highlightAll()
一次。这在此CodePen示例中进行了演示。抱歉回复晚了。
很棒的东西,感谢分享!我正在学习HTML/CSS/JS,所以我还无法真正实现它,但我了解此解决方案的本质,这是一个非常酷的想法。
嗨!这是一个很棒的练习!我遇到了一些问题,我会在这里询问,以防有人遇到同样的问题...
我的问题是,两个滚动条只在我在末尾添加一个新行时才不同。问题是,一旦我添加一个新行,并且在我写入任何字母之前,textarea的滚动条就会更高,因为突出显示器还没有添加它,直到写入一些内容,我不知道该怎么办。
有什么帮助吗?
谢谢!
我回复我自己,因为答案已经包含在帖子信息中...但是!
但在我的情况下,现在每行都有一个空格作为第一个字符,所以每次我添加一个新行,它都会插入一个空格,这不是一件好事。
为什么在你身上不会发生呢?
再次感谢!
对于延迟,我感到非常抱歉;感谢您提到这一点。
您的情况表明您可能在将
" "
附加到textarea
的值而不是用于在pre code
中显示的临时变量(在本文章中称为text
)。我希望这会有用;如果不是这种情况,请告诉我。我希望我的可编辑区域在输入时沿x轴滚动。
我该怎么做?
(对于延迟回复感到抱歉;在过去几周里,我一直在忙。)
当前的演示已经支持在两个轴上滚动(请参阅滚动和调整大小),一旦一行文本的长度超过了输入宽度。
为了确保代码输入元素不会长得过大,这可能会导致问题,请确保您已将宽度设置为(您想要的宽度)-32px。
我希望这能解决您的问题;如果不能,请告诉我。
嗨Oliver Geer,在这个代码示例中出现了一个问题,每当我通过突出显示方法粘贴代码以及调用更新(来自
<code>
的突出显示内容)方法来更新textarea上的相同内容时,代码会自动在左侧留出空格,我不知道如何解决。(很抱歉,我有一段时间没有注意到这一点。)
看起来您可能在这种情况中复制了缩进以及代码,因此左侧会出现一个看似空白的部分。您可以手动删除它,或者使用更多的JS来查找粘贴代码中每行的最小缩进量,然后在每行的开头删除此数量的空格。
我希望这会有用。
onpaste
事件(MDN链接)应该可以帮助您在粘贴文本时触发代码,并获取粘贴的代码。出于好奇,您将如何扩展
check_tab
以启用缩进所有选定文本。现在,选择多行会导致将选择替换为单个制表符。您可以使用一些 JS 逻辑来识别行(
element.value.split("\n")
),然后在循环中* 添加/删除(如果按住 Shift 键)缩进
* 同时计算出基于这些缩进移动
selectionStart
和selectionEnd
的位置值得一提的是,我已经在 GitHub 上名为
code-input
的自定义元素 JavaScript 库中 放置了此功能的更加可定制版本,以及 您所询问的代码位于indent
插件中。如果您要在项目中使用此组件,我建议使用该库 - 它完全开源并得到维护,并且旨在让您选择只保留您想要的特性。本文是对基本知识的良好介绍(并且早于该库),但该库具有更多功能。