65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 AngularJS 的可重用 HTML 编辑器控件。

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2020 年 11 月 1 日

MIT

14分钟阅读

viewsIcon

7453

downloadIcon

159

如何设计一个可预览的 HTML 编辑器,以及如何在您的应用程序中使用此控件

引言

这个教程是我为 Code Project 创建过的最好的教程。不久前,我使用 requirejs 和 stapesjs 编写了一个类似的教程。在那篇文章中,我曾承诺会继续发布一个使用 AngularJS 的教程。在那篇教程完成后,我立刻就开始着手编写示例程序。当我完成这个应用程序时,它的效果远超我的预期。当你查看代码并理解了它的工作原理后,你会同意这非常棒。

HTML 编辑器是作为一个 AngularJS 指令创建的,因此可以放置在任何使用 AngularJS 编写的 Web 应用程序中。本教程将首先讨论这个 HTML 编辑器控件的设计。然后讨论如何在您的应用程序中使用它。对于用 AngularJS 指令创建的任何控件来说,最大的挑战是数据交换。也就是说,将数据从宿主传递到指令相对容易,但将数据传递回宿主却很困难。我知道有 broadcastemit。您不想使用它们,因为它们可能不可预测且速度非常慢。本教程将向您展示双向变量绑定的方法,以便在宿主和指令之间交换值更改。为了正确地做到这一点,我不得不发挥创意。本教程将展示我使用过的技巧。在本教程中,我还将讨论如何从编辑器 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.jshtmlEditor.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 提供了 $scengBindHtml(或 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 获取高亮显示的文本,我们可以使用 selectionStartselectionEnd 属性。第一个返回高亮文本第一个字符的位置。第二个返回选定文本最后一个字符的位置。有了这两个位置,我就可以将文本切片成三部分。

如果没有高亮显示的文本呢?嗯,很简单,在这种情况下,开始位置和结束位置是同一个位置。所以三部分的第二部分是空字符串。我们仍然有三个部分。

以下代码捕获了选定文本的开始和结束索引,以及 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 日 - 初稿
© . All rights reserved.