起因

前两天我搞的那个在线 HTML 新标签页预览功能 https://www.cnblogs.com/duyuanshang/p/18829312 ,很好用,最近在我工作上帮了很大的忙,它可以让我不用打开本地代码编辑器就能快速调试一些前端代码,我也逐步完善它的了很多功能。可是,很快它就有了一个问题: 使用 <textarea> 写代码很不舒服。

当然,使用它来接受粘贴过后的代码还好,如果想再编辑一下、删删改改调整调整缩进,那简直难受死了。

最显而易见的解决方案,是使用一个第三方的 在线代码编辑器 的 js 库,像如 Code MirrorAce 这些成熟又著名的编辑器,可是..... 它们体积有点大,虽然大力压缩处理后也不过 100kb 左右,但依然重量级。

在很久以前,我就感觉到代码编辑框的重要性了,很多前端页面都会有在线写一些脚本的需求,如果使用频率非常高的话一般就引入第三方库了,但我真的不想引入,觉得德不配位,于是我看着那个 textarea 框开始发呆。我在想,有没有办法能让它在写代码时,体验好一点。

于是这几天我就捣鼓了一个很迷你的 js 库,https://github.com/kohunglee/areaEditor areaEditor.js,用着还真挺舒服。这个是一个演示 : https://www.ccgxk.com/areaEditor.html

单击这里

(又花了半个月,逐渐优化到能出山了,虽然用习惯了 IDE 的人可能觉得它用处不大,但 textarea 是真的没法用,况且它只有 2kb!小到忽略不计,作用却不容小觑,所以一定会有人能用到!)

缩进功能

最开始,我找了很多资料,发现貌似没人有针对 <textarea> 的直接优化。代码编辑器是很多,大部分都是另辟一大堆 <div> 模拟新编辑框了,或者直接在大轮子 Code Mirror 基础上进一步改进。

其实,有时我们需要的并不多。比如,这些代码编辑器有高亮的功能,能五颜六色显示关键词,其实这个属于「消费升级」的非刚需功能了,真正在编辑时的刚需是 代码缩进

代码缩进 > 自动缩进 >> 高亮提示 > 智能补全 > 括号补全  > 纠错提示 > 语法高亮

我们肯定很讨厌在 <textarea> 写代码时,一按 TAB 键,然后光标跑外星的那种感觉。其实我们完全可以使用 js 来改良这个。

(可以试试在 https://www.ccgxk.com/cellhtmleditor.html 这个实时编辑页面进行调试下面的这些代码 ~)

<textarea id="editor" placeholder="演示 tab 键缩进"></textarea>
<script>
editor.addEventListener('keydown', function(e){
var start = e.target.selectionStart; // 光标开始的位置
var end = e.target.selectionEnd; // 光标结束的位置
var value = e.target.value; // 编辑器里的内容 if (e.key === 'Tab') { // 按下 TAB 键
e.preventDefault(); // 阻止默认事件
e.target.value = value.substring(0, start) + '\t' + value.substring(end); // 添加 tab 字符
e.target.selectionStart = e.target.selectionEnd = start + 1; // 光标向前 +1
}
});
</script>

首先,我们阻止了 tab 的默认事件(跳到下一个表单元素)e.preventDefault(); ,然后我们调用 textarea 元素的 .value api 重新为其填充内容。

然后是 e.target.selectionStart = e.target.selectionEnd = ... 让我们的光标位置放到应该到达的地方。

.selectionStart.selectionEnd.value 这三个 API 现在实现这样一个小功能,然后还是这三个 API ,渐渐贯彻了整个代码。一切从这里开始。

缩进类型

目前,缩进格式并不统一,主流的缩进是 4 个空格,所以,就得需要能切换。

(可以试试在 https://www.ccgxk.com/cellhtmleditor.html 这个实时编辑页面进行调试下面的这些代码 ~)

<textarea id="editor" placeholder="演示缩进类型"></textarea>
<script>
editor.addEventListener('keydown', function(e){
var type = 'space'; // 缩进类型
var indentCount = 4; // 缩进的字符数量
var tabChar = (type === 'tab') ? '\t' : Array(indentCount + 1).join(' '); // 缩进字符 var start = e.target.selectionStart; // 光标开始的位置
var end = e.target.selectionEnd; // 光标结束的位置
var value = e.target.value; // 编辑器里的内容 if (e.key === 'Tab') { // 按下 TAB 键
e.preventDefault(); // 阻止默认事件
e.target.value = value.substring(0, start) + tabChar + value.substring(end); // 添加 tab 字符
e.target.selectionStart = e.target.selectionEnd = start + indentCount; // 光标向前 +1
}
});
</script>

代码里 Array(indentCount + 1).join(' ') 是一种技巧,可以返回 n 个空格,比如 Array(5 + 1).join(' ') 就是 5 个空格,Array(3 + 1).join(' ') 就等于 3 个空格。

根据这个原理,代码的收缩也实现了。不过,还是有很多坑,习惯上的坑:

  • 收缩后,光标的选择怎么计算?
  • 如果光标的 start 位于缩进,比如空格上,又该怎么计算??
  • ...

增加缩进很简单,减少缩进没想到这么复杂,计算逻辑还挺抽象,花了好几个小时润色,才算完美解决,下面是最终的代码:

// TAB 键盘的处理
if (e.key === 'Tab') {
e.preventDefault();
if (start === end) { // 光标未选中多个字符
e.target.value = value.substring(0, start) + this.tabChar + value.substring(end);
e.target.selectionStart = e.target.selectionEnd = start + this.tabLength;
return;
} else {
var contentArr = value.split('\n');
var contentArrOriginal = value.split('\n');
var startLine = (value.substring(0, start).match(/\n/g) || []).length;
var endLine = (value.substring(0, end).match(/\n/g) || []).length;
if (event.shiftKey) { // 按下 Shift 键(减少缩进)。
for (var _i = startLine; _i <= endLine; _i++) {
contentArr[_i] = this._removeLeadingSpaces(contentArr[_i], this.tabLength);
}
e.target.value = contentArr.join('\n');
var lengthDiff = contentArrOriginal[startLine].length -
contentArrOriginal[startLine].trimStart().length; // 计算光标起始点位于那一行
var moveLength = Math.min(this.tabLength, lengthDiff); // 计算最小可缩进值,以防止起始位置(如行5)缩进至行4。
var limitLineNum = this._arrSum(contentArr, startLine); // 处理选区起始在缩进(空白)处的情况。
var startPoint = limitLineNum > start - moveLength - startLine ? limitLineNum + startLine : start - moveLength; e.target.selectionStart = lengthDiff > 0 ? startPoint : start;
e.target.selectionEnd = end - (contentArrOriginal.join('\n').length - e.target.value.length);
} else { // 单独按 Tab 键(增加缩进),这个简单,文章上面已经写了
for (var _i = startLine; _i <= endLine; _i++) {
contentArr[_i] = this.tabChar + contentArr[_i];
}
e.target.value = contentArr.join('\n');
e.target.selectionStart = start + this.tabLength;
e.target.selectionEnd = end + this.tabLength * (startLine === endLine ? 1 : endLine - startLine + 1);
}
}
}

自动补全括号

但,只有这种缩进,肯定还不够,我们还需要那种写下括号后,再回车,产生的那种自动换行和缩进的效果。

按下回车:

顺便把自动括号也实现。

于是哐哐一顿实现,计算还简单,但逐渐发现一个问题。

这是程序 1.0 时候的一个案例:

<textarea placeholder="演示 enter 键 bug"></textarea>
<script src="https://cdn.jsdelivr.net/gh/kohunglee/areaeditor/src/areaeditor.1.0.x.min.js"></script>
<script>
var editor = new AreaEditor('textarea');
</script>

当我们用中文输入法输入时,一回车(按照习惯,应该只会输入 jtxql 这六个字符),会产生这样的效果:

显然使用 addEventListener('keydown', ... ) 是不行的。

于是又加入了 addEventListener('input', ... )keydowninput 是不一样的。前者是在键盘按下的一瞬间触发,在字符输入前(也因此可以阻止字符输入),而后者 input 是在字符输入后探测你按下了哪个按键。

犯难

这就犯了难,我到底是使用哪个来监测 enter 键的按下。如果是前者,那我避免不了这个 bug,如果是后者,那画面会跳动一下,很诡异。

最后我还是使用后者,只不过我不是监测的按钮,而是字符 if(lastChar === '\n'){ ... } ,然后重新生成缩进内容,里面多来一个换行符即可。

自动补全

自动补全倒是没有什么好说的。唯一要说的,【若用户仍选择手动完成,则忽略】这个功能,让这个自动补全变得非常流畅。但我没想到,要判断用户有没有手动补全,竟然要判断三个布尔:前一个字符,是否属于要补全的符号,后一个字符是否等于应补全的符号,用户输入的是否等于已经补全的符号。

var autoPairs = {
'{': '}',
'[': ']',
'(': ')',
'"': '"',
"'": "'",
'`': '`',
};
if (['{', '(', '[', '"', "'", '`', ']', '}', ')'].includes(lastChar) && start === end) {
if(this.isPreventAuto){
this.isPreventAuto = false;
return;
}
var pairChar = autoPairs[lastChar] || '';
for (var leftBrace in autoPairs) { // 若用户仍选择手动完成,则忽略
if (leftBrace === secondLastChar && autoPairs[leftBrace] === lastChar && nextChar === lastChar) {
e.target.value = value.substring(0, start) + value.substring(start + 1);
e.target.selectionStart = e.target.selectionEnd = start;
return;
}
}
e.target.value = value.substring(0, start) + pairChar + value.substring(start);
e.target.selectionStart = e.target.selectionEnd = start;
}

阻止补全

其实,上面的这些行为,并不能一直都有效。所以我又设立了阻止补全。不能说我们按快捷键复制粘贴的时候,也顺手补全了,也不能说我们在按删除键的时候也给补全了(o( ̄▽ ̄)d 这样就陷入死循环了 ~ 一个永远也删不掉的右括号)

AreaEditor.prototype.isPreventAuto = false;  // 是否阻止某些自动脚本

检测到一些按键 或 粘贴事件 addEventListener('paste', ...); 后,就不再执行。

编辑框抖动

一个不知起源于何时的 textarea 特性,当行数比较大时,回车会让输入框抖一下,十分影响..... 起码影响心情:

(可以试试在 https://www.ccgxk.com/cellhtmleditor.html 这个实时编辑页面进行调试下面的这些代码 ~)

<textarea id=demoEditor rows=10 placeholder="演示编辑框抖动"></textarea>
<script src="https://cdn.jsdelivr.net/gh/kohunglee/areaeditor/src/areaeditor.1.0.js"></script>
<script>
var content = '';
for(var i = 0; i < 200; i++){
content += i+'\n'
}
demoEditor.value = content;
</script>

于是

这是浏览器的原生特性(bug),意义不明。

当然,这个问题好解决,只需要记录下高度,在完成我们的操作后将高度还原即可。

在空行按下删除键,清空

在一个只存在缩进、空格的行,我们按下删除键,不出意外,目的只有一个,就是将这行删干净。所以,我又加上这样一个功能,在空行按下删除键,清空。

本以为只是一两行代码才能完成,最后搞了一坨:

if (e.key === 'Backspace') {
var contentArr = value.split('\n');
var startLine = (value.substring(0, start).match(/\n/g) || []).length; // 当前行仅包含空格和制表符
if(start === end && (/^[\s\t]*$/.test(contentArr[startLine]) && contentArr[startLine] !== '')){
e.target.selectionStart = this._arrSum(contentArr, startLine) + startLine;
e.target.selectionEnd = start;
}
}

体验感大大的好。

封装代码

我想把它做成一个第三方的引用库,那么我就要尽可能写的标准一点。

模仿 jQuery、Zepto 将它封装了一下。

首先是 UMD 模块化,

(function (global, factory) {
if (typeof define === 'function' && define.amd) { // UMD 模式
define([], factory); // AMD
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(); // CommonJS
} else {
global.AreaEditor = factory(); // 这样写,可以不用 new 关键字来调用
}
}(this, function () {
'use strict'; // 构造函数
function AreaEditor(element, options = {indentType : { type: 'space', count: 4 }}) { ..... ..... return AreaEditor;
}));

第一个,是依照过去我们常用的 requireJS 要求的格式来定义的,这是一个模块化工具,让我们的 JS 文件们可以按需加载。虽然在 ES6 时代日薄西山,但还是能用得到。学习这个可以看阮一峰大佬的这篇文章

第二个是 CommonJS 环境使用的,也就是服务器端。主要用于 node.js 。可以供 require('./logger.js') 这种语法使用。

第三个就是我们现在使用的这种方式,即浏览器直接调用。

里面的 factory() 是工厂函数, 'use strict'; 及以下就是这个函数的内容。我们只需将我们的 AreaEditor 函数写入即可。

AreaEditor() 是构造函数,通常使用大写字母开头。就好像一个对象一样,不过调用的时候需要写 new ,有了上面的 factory() 的处理后,不写 new 关键字也可以。


怎么压缩 JavaScript 代码

初步压缩,主流的有三个选择:

第一个是谷歌公司使用 Java 搞的智能压缩,可以分析代码把冗余给铲除,确保结果不变。不过效果和下面两个差不多。

第三个 Terser 我们可能都间接用过,它是 webpack 这个打包工具的默认压缩工具。其实它是在 UglifyJS 基础上迭代的。

在这里,我使用了 UglifyJS 。它也会智能将代码里的多余的地方优化,以实现尽可能小的体积:

var a=1;var a=2;
// 合并重复变量
var a=2 alert('a' + 'b');
// 优化
alert("ab"); function a(){
var info = 'a' + 'b';
alert(info);
}
// 优化
function a(){alert("ab")}

它本身是一个 node.js 库,无法直接在浏览器上运行,不过有大佬将其转化为了浏览器端。我又将其配置表和界面给翻译成中文,就是下面这个地址:

https://git.ccgxk.com/jscompression/jsminifier.html

使用它生成后的代码,还没有到 2kb 这个阶段,然后我又找到了个利用字母出现频次,构成字典,然后进行压缩的 js 压缩工具。

常出现在一些 代码高尔夫 炫技比赛里,比比谁能用更小的体积实现更复杂的功能或游戏这种比赛里,类似于 https://js13kgames.com/

我把那个界面搞的漂亮了一点,然后将全局变量改了一下名,就是这个页面:

https://git.ccgxk.com/jscompression/jscrush.html

这样就差不多 2kb 了。

但是,这个 js crush 压缩,其实在有 Gzip 的服务器情况下,并不是必需品,Gzip 的压缩率和这个差不多。

更多的功能?

以后不会添加更多的功能了,因为没必要 ~ textarea 的特性就决定了它无法完美实现代码高亮,而自动语法又是很复杂的事情,需要大量代码,所以没有必要。

我的这个 areaEditor.js 的存在意义是,为那些极端情况下准备的:比如 在线改一些简单的代码、一些轻量级的库、一些对网络有限制的场景、一些简单的页面、一些黑白页面....

另外,功能再多一点,就到「结界」了:毕竟体积大了,我为什么不选择更专业的?

自制体积不到 2kb 的代码编辑器,areaEditor.js,增强 textarea 标签的代码编辑体验的更多相关文章

  1. 微软良心之作——Visual Studio Code 开源免费跨平台代码编辑器

    微软良心之作——Visual Studio Code 开源免费跨平台代码编辑器 在 Build 2015 大会上,微软除了发布了 Microsoft Edge 浏览器和新的 Windows 10 预览 ...

  2. CodeMirror:基于JavaScript的代码编辑器

    官方网站定义: http://codemirror.net/ CodeMirror is a versatile text editor implemented in JavaScript for t ...

  3. 使用代码编辑器Sublime Text 3进行前端开发及相关快捷键

    推荐理由: Sublime Text:一款具有代码高亮.语法提示.自动完成且反应快速的编辑器软件,不仅具有华丽的界面,还支持插件扩展机制,用她来写代码,绝对是一种享受.相比于浮肿沉重的Eclipse, ...

  4. 2018-06-18 sublime代码编辑器

    sublime代码编辑器:主流前端开发编辑器,体积较小,运行速度快,文本功能强大,支持VI扩展,linux下面有个自带的文本编辑神器,名字叫做:vi ,熟练vi模式可以大量减少使用鼠标的次数,从而增加 ...

  5. 代码编辑器Sublime Text 3 免费使用方法与简体中文汉化包下载

    Sublime Text这款代码编辑器是Jeff 一直都在使用的,前段时间转用到版本3,因为感觉Sublime Text 3 启动速度更加快,运行更加流畅——虽然3 还是在Beta 阶段.下面就直接分 ...

  6. NanUI for Winform 使用示例【第一集】——山寨个代码编辑器

    NanUI for Winform从昨天写博客发布到现在获得了和多朋友的关注,首先感谢大家的关注和支持!请看昨天本人的博文<NanUI for Winform发布,让Winform界面设计拥有无 ...

  7. 轻量、强大的代码编辑器控件-WinForm完美版

    前段时间做个小项目需要用到一个代码编辑器控件,但网上搜了半天,居然没发现一个完全满意的编辑器.现有的一些编辑器有: FastedTextBox 优点:1.  轻量. 2. 样式美观. 3. DEMO完 ...

  8. 在线代码编辑器CodeMirror简介

    1.什么是Code Mirror 最近做一个项目需要在网页上实现一个代码编辑器,支持语法高亮.自动缩进.智能提示等功能.发现Code Mirror刚好满足所有需求.Code Mirror是由js写的一 ...

  9. (视频) 《快速创建网站》 3.2 WordPress多站点及Azure在线代码编辑器 - 扔掉你的ftp工具吧,修改代码全部云端搞定

    本文是<快速创建网站>系列的第6篇,如果你还没有看过之前的内容,建议你点击以下目录中的章节先阅读其他内容再回到本文. 访问本系列目录,请点击:http://devopshub.cn/tag ...

  10. Sublime Text 2 - 性感无比的代码编辑器!程序员必备神器!跨平台支持Win/Mac/Linux

    我用过的编辑器不少,真不少- 但却没有哪款让我特别心仪的,直到我遇到了 Sublime Text 2 !如果说“神器”是我能给予一款软件最高的评价,那么我很乐意为它封上这么一个称号.它小巧绿色且速度非 ...

随机推荐

  1. Typecho实现版权声明的三种方式

    在安装完Typecho之后,第一件事应该就是想着如何去折腾了.对于个人博客而言,不希望自己辛辛苦苦写的文章,被别人转载或无脑采集,还不留原地址,所以就需要在文章的末尾地方放上一个版权声明,来提醒下转载 ...

  2. LCP 06. 拿硬币

    地址:https://leetcode-cn.com/problems/na-ying-bi/ <?php /** * Class Solution * 桌上有 n 堆力扣币,每堆的数量保存在数 ...

  3. SQL Server 2025 AI相关能力初探

    SQL Server 在2024年11月开始进行社区私有预览(链接),由于涉及AI能力,我也是第一时间申请了内侧资格,悲剧的是,直到2025年2月,才拿到预览版的测试资格-.-,此时已经是CTP1.3 ...

  4. 【Pre】预习测试-Logisim/Verilog/MIPS

    好家伙,开门挂 T2 字符自动机cscore 1.审题·惯性思维:直接输出了当前连续数,题目要求是最大连续数 -> [重新读题解决] 2.非阻塞赋值运用:若在always块内通过if(out1 ...

  5. 多智能体粒子环境(Multi-Agent Particle Env)食用指南--从入门到入土

    0.项目地址: 原地址:openai/multiagent-particle-envs: Code for a multi-agent particle environment used in the ...

  6. Bash Shell 30min 过家家

    带你捅破窗户纸 - 备注 : @博客园 : 1. 为什么不支持 pdf 上传了呀 2. 网站分类不好用 3. 排版OA工具升级下, 例如 markdown 写出来好丑. 尝试升级下呢 ? 后记: 学如 ...

  7. mac zsh: command not found: python

    前言 在 mac 上安装 python 后,会自动在 .zprofile 文件中,加入: # Setting PATH for Python 3.12 # The original version i ...

  8. goland无法识别包

    新建 Go 项目时,一定要通过 "File -> New -> Project..." 方式建立,千万不要通过 "File -> Open", ...

  9. go包管理工具 govendor

    govendor介绍 govendor 是 GoLang 常用的一个第三方包管理工具,它的出现解决了不同用户在 clone 同一个项目时从外部获取不同依赖库版本的问题. govendor会将项目需要的 ...

  10. 【FAQ】HarmonyOS SDK 闭源开放能力 —Push Kit(10)

    1.问题描述: 离线推送,锁屏的时候没有弹出消息,只有下拉在通知中心里面显示.请问是否是正常的? 解决方案: 检查一下是否存在图片风控:https://developer.huawei.com/con ...