使用 AngularJS 的可重用 HTML 编辑器控件。
如何设计一个可预览的 HTML 编辑器,以及如何在您的应用程序中使用此控件
引言
这个教程是我为 Code Project 创建过的最好的教程。不久前,我使用 requirejs 和 stapesjs 编写了一个类似的教程。在那篇文章中,我曾承诺会继续发布一个使用 AngularJS 的教程。在那篇教程完成后,我立刻就开始着手编写示例程序。当我完成这个应用程序时,它的效果远超我的预期。当你查看代码并理解了它的工作原理后,你会同意这非常棒。
HTML 编辑器是作为一个 AngularJS 指令创建的,因此可以放置在任何使用 AngularJS 编写的 Web 应用程序中。本教程将首先讨论这个 HTML 编辑器控件的设计。然后讨论如何在您的应用程序中使用它。对于用 AngularJS 指令创建的任何控件来说,最大的挑战是数据交换。也就是说,将数据从宿主传递到指令相对容易,但将数据传递回宿主却很困难。我知道有 broadcast
和 emit
。您不想使用它们,因为它们可能不可预测且速度非常慢。本教程将向您展示双向变量绑定的方法,以便在宿主和指令之间交换值更改。为了正确地做到这一点,我不得不发挥创意。本教程将展示我使用过的技巧。在本教程中,我还将讨论如何从编辑器 textarea
获取文本值,将其转换为 HTML 并附加到作为预览区域的 div。这里的技巧是,每次用户输入一个字符时,更改都会反映在预览区域。这一点将在下面解释。
这个应用程序是什么样的
在我们深入研究之前,让我们先看看这个示例应用程序是什么样的
顶部的两个按钮是宿主区域的一部分。它们用于测试从指令获取值。暂时不用担心它们。在两个按钮下方有一个小按钮(标记为“+”)。单击它,可以展开并显示预览区域。最初,预览区域将显示为空。
在您输入 HTML 内容的文本区域上方,有两个下拉按钮,每个按钮都有一个扩展的选项列表,可以选择将 HTML 标签添加到文本区域。您也可以突出显示 textarea
中的一些文本,然后单击下拉列表中的一个选项,HTML 标签将被添加到突出显示的文本周围。
是的。这不是一个“所见即所得”的 HTML 编辑器。它是一个老式的代码编辑器,您必须输入 HTML 代码和文本,并希望它能正常显示。我只是通过一个可隐藏的预览区域使其稍微好用了一些。您输入的内容应该会立即显示出来,这样您就可以在看到错误时进行修复。
如果您不认为这很酷。那么请停止阅读,去别的地方吧。我们不想浪费您的时间。如果您有兴趣继续阅读,太好了!让我给您展示最后两张截图。这是我将要放入文本区域的 HTML 内容
预览区域将显示此内容
这两个列表显示了从下拉菜单中可用的所有 HTML 标签
从截图和一些猜测可以看出,您知道该应用程序和 HTML 编辑器使用了 bootstrap 进行标记。因此,这两个可选 HTML 标签的列表也使用了 bootstrap 的 CSS 类。如果您想使用一组不同的 CSS 类,请自行修改应用程序。祝您好运。
我希望这些截图能让您确信想要继续阅读并享受所有秘密。让我们来看看第一个秘密。
AngularJS 代码与 textarea 之间的交互
这是一个相当复杂的示例应用程序。所有的复杂性都集中在指令的定义中。为了节省您阅读枯燥的细节,我将只展示指令中最重要的部分。如果您想了解全貌,请查看 HTML 编辑器指令的完整源代码。
从 TextArea 获取文本内容
我们都知道如何通过纯 JavaScript 或 JQuery 获取 HTML 元素的引用。如果您读过我的一些旧教程,您可能知道这是通过 AngularJS 完成的。这只是一个回顾。
编辑器有一个 textarea
,其定义如下(请参阅文件 htmlEditor.html)
<textarea id="contentSource" class="form-control editor-textarea" rows="12">
</textarea>
在指令定义(htmlEditor.js 或 htmlEditor.sj 文件)中,我使用了超级助手函数 angular.element()
来获取对这个 textarea
的引用。这个引用不会改变,因为它是一个静态元素在页面上。一旦我有了引用,我就不必再次获取它。然后获取和设置它的值就会很简单。以下是获取引用以及获取/设置 textarea
值的代码
...
vm.htmlEditorTextArea = angular.element("#editorArea #contentSource");
...
...
vm.htmlEditorTextArea.val(fmtText); // setting value to the text area
...
...
$scope.htmlText = vm.htmlEditorTextArea.value; // getting value from the text area.
...
这是简单部分。接下来是稍微难一点的部分。
TextArea 与另一个 HTML 元素之间的数据交换
接下来,我将讨论如何处理用户输入某些字符时,更改会立即显示在预览区域的情况。为了实现这一点,我必须使用一个作用域变量,从 textarea
获取值,然后将此作用域变量绑定到预览区域。这种设置的优点是,textarea
中的任何更改都会传递到这个作用域变量。然后 $scope.$apply()
会自动将更改应用到作用域变量。
技巧在于,textarea
的任何更改都必须通知指令以更新作用域变量的值。这需要一些研究。基本上,任何 HTML 输入元素都可以附加一些事件处理程序。也就是说,这些事件中的任何一个都可以触发调用一些自定义函数。在这种情况下,有四个不同的事件可以附加事件处理程序
- "
input
",当textarea
的值发生更改时,会触发此事件。 - "
change
",当textarea
的值即将更改时,会触发此事件。您可以看到的值已更改,但更改后的值尚未设置到textarea
。这是一个中间事件。 - "
focus
",当textarea
获得焦点时,会触发此事件。 - "
blur
",当textarea
失去焦点时,会触发此事件。
我为所有这些事件都添加了相同的事件处理程序。一旦它们被触发,textarea
上的值将被更新到作用域变量。以下是我的做法
...
vm.htmlEditorTextArea = angular.element("#editorArea #contentSource");
...
vm.htmlEditorTextArea.on("input", function () {
updateHtmlText(this);
});
vm.htmlEditorTextArea.on("change", function () {
updateHtmlText(this);
});
vm.htmlEditorTextArea.on("focus", function () {
updateHtmlText(this);
});
vm.htmlEditorTextArea.on("blur", function () {
updateHtmlText(this);
});
...
...
function updateHtmlText(ctrlRef) {
if (ctrlRef != null) {
$scope.htmlText = ctrlRef.value;
$scope.$apply();
}
}
调用 $scope.$apply()
是必要的,因为这些事件处理函数超出了 AngularJS 代码的范围,所以即使我将值赋回给作用域变量 $scope.htmlText
,它仍然没有值。$scope.$apply()
就像提交一个事务。它完成了事务。
当您查看指令的源代码时,您会看到指令的定义如下
(function () {
"use strict";
var mod = angular.module("htmlEditorModule", [ ]);
mod.directive("htmlEditor", [ function () {
var htmlEditorController = ["$scope", "$sce", "htmlEditService",
function ($scope, $sce, htmlEditService) {
...
return {
restrict: "EA",
templateUrl: "/assets/app/pages/directives/htmlEditor/htmlEditor.html",
scope: {
htmlText: "="
},
controller: htmlEditorController,
controllerAs: "vm"
};
}]);
})();
您能看出作用域变量 $scope.htmlText
在哪里定义的吗?在这里
...
scope: {
htmlText: "="
},
...
“scope
”是隔离作用域,htmlText
用于双向绑定。它绑定到宿主上的一个数据模型变量,这意味着宿主中的初始值将被传递到指令。而 $scope.htmlText
的值更改将被传递回宿主。有了这种设置,textarea
中的任何更改都会一路传播到宿主。而且不再需要使用 $broadcast()
或 $emit()
。
一旦您回顾了代码,理解了所有这些,太好了!您已经掌握了最难的部分。从这一点开始,事情会变得容易。接下来的部分,不像这个那么棘手,是如何将文本区域中的文本显示为 HTML。
将文本显示为 HTML 元素
使用 AngularJS 显示文本作为 HTML 很简单。AngularJS 提供了 $sce
和 ngBindHtml
(或 ng-bind-html
)来处理这个问题。为了实现这一点,我必须使用一个数据模型变量。我必须将值从作用域变量传递到这个数据模型变量。然后将数据模型变量绑定到视图。预览区域的定义如下
<div class="row html-preview-outer collapse" id="previewArea">
<div class="col-xs-12">
<div class="alert alert-info info-display">
You are preview the content.
</div>
</div>
<div class="col-xs-12 html-preview">
<div ng-bind-html="vm.htmlContent"></div>
</div>
</div>
使用的数据模型变量是 vm.htmlContent
。问题是这个数据模型不能直接获取值。宿主和指令之间的数据交换是指令的作用域变量 $scope.htmlText
。为了也将值传递给这个数据模型变量,我必须使用一个观察者(通过 $scope.$watch()
)。这里是
$scope.$watch("htmlText", function(newVal, oldVal) {
if (newVal != null && newVal.length >= 0 && newVal !== oldVal) {
vm.htmlEditorTextArea.val(newVal);
vm.htmlContent = $sce.trustAsHtml(newVal);
$scope.htmlText = newVal;
}
});
此观察者设置做了几件事。它检查新值是否不为 null
、不为空且与旧值不同。如果为 true
,那么
- 将
textarea
的值设置为这个新值。 - 设置数据模型变量
vm.htmlContent
的值。这将把更新的 HTML 内容显示在预览区域。 - 将作用域变量设置为新值。
这似乎有点多余。为什么这是必需的?这是必需的,因为在我进行测试时,最初的设计不起作用。textarea
中的 HTML 文本值发生了变化,作用域变量也发生了变化。但不知何故,更改从未传递到宿主。所以我使用了这种三向数据交换,它为我解决了这个问题。不要误以为有三个相同的 HTML 文本内容副本。该值是按引用传递的,因此应该只有一个副本。
下一个有趣的部分是如何用 HTML 标签修饰文本区域中的值。这意味着我需要获取突出显示的文本(所选文本的第一个和最后一个字符的索引位置),然后在选定文本之前或之后插入 HTML 标签,这将在下一节中进行解释。
切片文本内容然后修饰
一个必备功能是用户通过突出显示文本来选择 textarea
中的某些文本。然后用户可以添加一个开始 HTML 标签和一个结束 HTML 标签。要做到这一点,我必须将文本分成三部分:第一部分是从开头到高亮文本开始前一个字符的文本。第二部分是高亮显示的文本。最后一部分是从高亮文本之后的第一个字符到文本末尾的文本。然后添加开始和结束标签就很简单了。
要从 textarea
获取高亮显示的文本,我们可以使用 selectionStart
和 selectionEnd
属性。第一个返回高亮文本第一个字符的位置。第二个返回选定文本最后一个字符的位置。有了这两个位置,我就可以将文本切片成三部分。
如果没有高亮显示的文本呢?嗯,很简单,在这种情况下,开始位置和结束位置是同一个位置。所以三部分的第二部分是空字符串。我们仍然有三个部分。
以下代码捕获了选定文本的开始和结束索引,以及 textarea
的整个文本字符串
function getSelectedTextSegment() {
var startPos = vm.htmlEditorTextArea.prop("selectionStart");
var endPos = vm.htmlEditorTextArea.prop("selectionEnd");
var wholeText = vm.htmlEditorTextArea.val();
if (startPos == null) {
startPos = 0;
}
if (endPos == null) {
endPos = 0;
}
if (wholeText != null && startPos > wholeText.length) {
startPos = wholeText.length-1;
}
if (wholeText != null && endPos > wholeText.length) {
endPos = wholeText.length-1;
}
return {
wholeText: wholeText,
startPos: startPos,
endPos: endPos
};
}
此函数获取高亮文本的开始和结束索引以及整个文本字符串。然后将它们包装成一个对象并返回。该对象将由 AngularJS 服务用来修饰 HTML 标签。此函数还处理意外情况。当字符串为空时,两个索引都将设置为 0
。如果任何索引超出了字符串长度,它将被设置为字符串的最后一个字符。
以下代码将 HTML 内容切片成三部分,然后添加标签,最后所有部分连接在一起
function addHtmlTagsToText (wholeText, selStart, selEnd, startTag, endTag) {
if (wholeText != null && wholeText.length > 0) {
if (selStart < 0) {
selStart = 0;
}
if (selEnd < 0) {
selEnd = 0;
}
if (selStart >= wholeText.length) {
selStart = wholeText.length - 1;
}
if (selEnd >= wholeText.length) {
selEnd = wholeText.length - 1;
}
if (selStart == selEnd) {
var startText = wholeText.substring(0, selStart);
var endText = wholeText.substring(selStart);
return startText + startTag + endTag + endText;
} else if (selStart >= 0 && selStart < selEnd && selEnd <= wholeText.length) {
var startText = wholeText.substring(0, selStart);
var middleText = wholeText.substring(selStart, selEnd);
var endText = wholeText.substring(selEnd);
return startText + startTag + middleText + endTag + endText;
} else {
return wholeText;
}
} else {
return "" + startTag + endTag;
}
}
该函数非常直观。首先,它会检查 textarea
的文本是否不为 null
。然后它会对高亮部分的开始索引和结束索引进行相同的检查,并在需要时进行“重新定位”。完成检查后,我使用 JavaScript 的 substring()
将整个字符串切片成三部分(如果没有高亮部分,则为两部分)。最后,字符串在添加了开始标签和结束标签到高亮部分之后重新组合,然后将第一部分、第二部分和第三部分加在一起形成一个整体。
这些都是这个可重用组件的关键部分。它们只是一个复杂组件的组成部分。如果您不想设计类似的东西,就不需要了解所有这些细节。接下来,我将向您展示如何使用这个可重用组件。
使用此 HTML 编辑器
为了演示如何使用这个 HTML 编辑器,我写了一个简单的应用程序。这个应用程序只有这个 HTML 编辑器和两个按钮。一个标记为“Load”的按钮,点击后会调用后端 RESTFul API 加载 HTML 内容并显示在 HTML 编辑器的 textarea
中,以及预览区域。另一个按钮“Save”将编辑器中的 HTML 内容通过 RESTFul API 推送到后端。两个按钮都是宿主的一部分,而不是指令的一部分。目的是演示指令和宿主之间的数据交换。
您可以在此应用程序的索引页面(index.html)上看到该指令的使用情况。它是这样声明的
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
...
...
<div html-editor html-text="vm.htmlText"></div>
...
...
</body>
</html>
这个(app.js 文件)是如何注入并定义要绑定到 HTML 编辑器指令的宿主数据模型变量的
(function () {
"use strict";
var mod = angular.module("testSampleApp",
[ "ngResource", "infoDisplayModule", "htmlEditorModule" ]);
mod.controller("testSampleController",
[ "$scope", "$sce", "$resource", "infoDisplayService",
function ($scope, $sce, $resource, infoDisplayService) {
var vm = this;
vm.htmlText = "";
...
}
]);
})();
我在这里高亮了重要部分。首先,为了访问该指令,我必须将包含该指令的模块注入到应用程序模块中。然后定义数据模型变量,即 vm.htmlText
。这两者和 HTML 元素是使用此 HTML 编辑器指令所需的一切。
在应用程序的 HTML 页面上,该指令被注入为一个名为 html-editor
的属性到一个 <div>
元素上。应用程序作用域的变量 htmlText
通过属性 html-text
以双向绑定的方式传递到指令。这就是双向绑定工作的方式。有了这个标记和将模块注入到应用程序中,整个应用程序就完成了。
接下来,我将描述如何进行测试。
如何测试应用程序
要构建示例应用程序,请使用命令行提示符,进入找到 pom.xml 文件的文件夹。然后运行以下命令
mvn clean install
要运行应用程序,构建完成后,键入以下命令并按 Enter
java -jar target\hanbo-agular-htmleditor-1.0.1.jar
等待启动运行完成,然后您将在最后看到类似这样的内容
...
...
2020-10-28 23:20:13.998 INFO 3460 --- [ main]
o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-10-28 23:20:14.129 INFO 3460 --- [ main]
o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page:
class path resource [static/index.html]
2020-10-28 23:20:14.273 INFO 3460 --- [ main]
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on
port(s): 8080 (http) with context path ''
2020-10-28 23:20:14.289 INFO 3460 --- [ main] org.hanbo.boot.rest.App
: Started App in 3.942 seconds (JVM running for 4.586)
如果您遇到任何错误,最有可能的原因是端口 8080 被其他东西占用了。请检查这一点并重新运行。
如果您能够成功运行,现在就可以使用浏览器导航到以下 URL
https://:8080
您将看到带有 HTML 编辑器的索引页面。尝试“Load”按钮。看看 HTML 内容是否填充到编辑器 textarea
中。并尝试下拉菜单中的所有标签命令,看看它们会显示什么。祝您玩得开心。最后,单击“Save”按钮,内容将发送到后端,您可以在命令行提示符的日志输出中看到它。
摘要
本教程的主题确实非常复杂。它是一个用 AngularJS 编写的可重用 HTML 编辑器,仅适用于基于 AngularJS 和 Bootstrap 的应用程序。这可能是我向公众提供的第一个可重用组件。我已经尽我所能测试了这个组件。我相信可能存在问题,请自行承担使用风险。
与我过去的一些教程不同,这篇教程没有那么详细。我只描述了关于这个组件的一些关键领域,例如如何获取 textarea
的文本内容,textarea
和预览区域之间的数据交换是如何进行的,以及指令和宿主之间的数据交换是如何完成的。这些是设计的重要方面。对于最终用户来说,这些都不重要。“使用此 HTML 编辑器”部分描述了如何在您的应用程序中使用它(只要它是基于 Bootstrap 和 AngularJS 的)。它最好用于占用整个页面的场景,因为它占用了大量的页面空间。
祝您愉快!2021 年再见!
历史
- 2020 年 10 月 28 日 - 初稿