Heim > Web-Frontend > js-Tutorial > Hauptteil

Implementieren Sie einen Rich-Text-Editor mit Javascript

php中世界最好的语言
Freigeben: 2017-11-18 13:20:59
Original
3868 Leute haben es durchsucht

Denn im aktuellen Projekt müssen wir einen Rich-Text-Editor entwickeln, der mit PCs und mobilen Endgeräten kompatibel ist und einige spezielle Anpassungsfunktionen enthält. Also habe ich mir die vorhandenen JavaScript-Rich-Text-Editoren angesehen. Auf der Desktop-Seite gibt es viele, auf der mobilen Seite jedoch fast keine. Die Desktop-Seite wird durch UEditor repräsentiert. Da ich jedoch vorerst keine Kompatibilität in Betracht ziehe, besteht keine Notwendigkeit, ein so umfangreiches Plug-In wie UEditor zu verwenden. Aus diesem Grund habe ich mich entschieden, Javascript zur Implementierung eines Rich-Text-Editors zu verwenden. In diesem Artikel wird hauptsächlich erläutert, wie Sie einen Rich-Text-Editor implementieren und einige Fehler zwischen verschiedenen Browsern und Geräten beheben.

Vorbereitungsphase

Viele APIs wurden für uns in modernen Browsern vorbereitet, damit HTML Rich-Text-Bearbeitungsfunktionen unterstützen kann. Wir müssen nicht den gesamten Inhalt selbst vervollständigen.

contenteditable=”true”
Nach dem Login kopieren

Zuerst müssen wir ein Div bearbeitbar machen, indem wir das Attribut contenteditable="true" hinzufügen.

<div contenteditable="true" id="rich-editor"></div>
Nach dem Login kopieren

Jeder Knoten, der in ein solches

eingefügt wird, kann standardmäßig bearbeitet werden. Wenn wir einen nicht editierbaren Knoten einfügen möchten, müssen wir das Attribut des eingefügten Knotens als contenteditable="false" angeben.

Cursorbetrieb

Als Rich-Text-Editor müssen Entwickler in der Lage sein, verschiedene Statusinformationen, Positionsinformationen usw. des Cursors zu steuern. Der Browser stellt Auswahlobjekte und Bereichsobjekte zum Bedienen des Cursors bereit.

Auswahlobjekt

Auswahlobjekt stellt den vom Benutzer ausgewählten Textbereich oder die aktuelle Position des Cursors dar. Es stellt eine Textauswahl auf der Seite dar, die sich über mehrere Elemente erstrecken kann. Textauswahlen werden erstellt, indem der Benutzer die Maus über den Text zieht.

Ein Auswahlobjekt abrufen

let selection = window.getSelection();
Nach dem Login kopieren

Normalerweise bedienen wir das Auswahlobjekt nicht direkt, sondern müssen die vom Benutzer ausgewählten Bereiche (Regionen) bedienen, die dem Auswahlobjekt entsprechen, was allgemein bekannt ist als „ Blau ziehen“. Die Erfassungsmethode lautet wie folgt:

let range = selection.getRangeAt(0);
Nach dem Login kopieren

Da der Browser derzeit möglicherweise über mehrere Textauswahlen verfügt, akzeptiert die getRangeAt-Funktion einen Indexwert. Bei der Rich-Text-Bearbeitung berücksichtigen wir nicht die Möglichkeit einer Mehrfachauswahl.

Das Auswahlobjekt verfügt außerdem über zwei wichtige Methoden: addRange und removeAllRanges. Wird verwendet, um der aktuellen Auswahl ein Bereichsobjekt hinzuzufügen bzw. alle Bereichsobjekte zu löschen. Sie werden später sehen, wie sie verwendet werden.

Bereichsobjekt

Das durch das Auswahlobjekt erhaltene Bereichsobjekt steht im Mittelpunkt unserer Cursoroperation. Der Bereich stellt ein Dokumentfragment dar, das Knoten und Teiltextknoten enthält. Wenn Sie das Entfernungsobjekt zum ersten Mal sehen, kommen Sie sich vielleicht seltsam und vertraut zugleich vor. Wo haben Sie es gesehen? Als Front-End-Ingenieur müssen Sie das Buch „JavaScript Advanced Programming Third Edition“ gelesen haben. In Abschnitt 12.4 stellt uns der Autor die von der DOM2-Ebene bereitgestellte Bereichsschnittstelle vor, um die Seite besser steuern zu können. Wie sah ich damals überhaupt aus? ? ? ? Was nützt das? Es besteht kein solcher Bedarf. Hier verwenden wir dieses Objekt ausgiebig. Für die folgenden Knoten:

<div contenteditable="true" id="rich-editor">
    <p>123</p>
</div>
Nach dem Login kopieren

Drucken Sie das Bereichsobjekt zu diesem Zeitpunkt aus:

* startContainer: Der Startknoten des Bereichs.

* endContainer: Der Endknoten des Bereichs

* startOffset: Der Offset der Startposition des Bereichs.

* endOffset: Bereichsoffset der Endposition.

* commonAncestorContainer: Gibt den tiefsten Knoten zurück, der startContainer und endContainer enthält.

* minimiert: Gibt einen booleschen Wert zurück, der verwendet wird, um zu bestimmen, ob die Start- und Endpositionen des Bereichs gleich sind.

Hier sind unser startContainer, endContainer und commonAncestorContainer alle #text-Textknoten „Baidu EUX Team“. Da sich der Cursor hinter dem Wort „degree“ befindet, sind sowohl startOffset als auch endOffset 2. Und es wird kein Widerstand erzeugt, daher ist der Wert von „collapsed“ wahr.

Aufgrund der Verzögerung sind startContainer und endContainer nicht mehr konsistent und der Wert von Collapsed wird falsch. startOffset und endOffset repräsentieren genau die Start- und Endpositionen des Drag Blue. Sie können es selbst ausprobieren, um weitere Effekte zu erzielen.

Um einen Bereichsknoten zu betreiben, sind die Hauptmethoden wie folgt:

setStart(): Setzt den Startpunkt des Bereichs

setEnd(): Setzt den Endpunkt des Bereichs

selectNode(): Legen Sie einen Bereich fest, der Knoten und Knoteninhalt enthält

collapse(): Reduzieren Sie den Bereich auf den angegebenen Endpunkt

insertNode(): Einfügen ein Knoten am Startpunkt des Bereichs.

cloneRange(): Gibt ein geklontes Range-Objekt zurück, das denselben Endpunkt wie der ursprüngliche Range hat

Es gibt so viele häufig verwendete Methoden in der Rich-Text-Bearbeitung, und es gibt noch viele weitere Methoden, die ich wird nicht aufgelistet.

Ändern Sie die Cursorposition

Wir können die Position eines Cursors oder den Ziehbereich ändern, indem wir die Methoden setStart() und setEnd() aufrufen. Die von diesen beiden Methoden akzeptierten Parameter sind ihre jeweiligen Start- und Endknoten und Offsets. Wenn ich beispielsweise möchte, dass sich die Cursorposition am Ende von „Baidu EUX Team“ befindet, können Sie die folgende Methode verwenden:

let range = window.getSelection().getRangeAt(0),
    textEle = range.commonAncestorContainer;
range.setStart(range.startContainer, textEle.length);
range.setEnd(range.endContainer, textEle.length);
Nach dem Login kopieren

Wir fügen einen Timer hinzu, um den Effekt zu sehen:

Diese Methode hat jedoch eine Einschränkung, wenn sich der Knoten ändert, auf dem sich der Cursor befindet. Wenn beispielsweise ein Knoten ersetzt oder ein neuer Knoten hinzugefügt wird, hat die erneute Verwendung dieser Methode keine Auswirkung. Aus diesem Grund benötigen wir manchmal ein Mittel, um eine Änderung der Cursorposition zu erzwingen. Der kurze Code lautet wie folgt (in der Praxis müssen Sie möglicherweise auch Selbstschließen und Elemente berücksichtigen):

function resetRange(startContainer, startOffset, endContainer, endOffset) {
    let selection = window.getSelection();
        selection.removeAllRanges();
    let range = document.createRange();
    range.setStart(startContainer, startOffset);
    range.setEnd(endContainer, endOffset);
    selection.addRange(range);
}
Nach dem Login kopieren

Wir Erstellen Sie ein Bereichsobjekt neu und löschen Sie die ursprünglichen Bereiche, um sicherzustellen, dass sich der Cursor definitiv an die gewünschte Position bewegt.

修改文本格式

实现富文本编辑器,我们就要能够有修改文档格式的能力,比如加粗,斜体,文本颜色,列表等内容。DOM 为可编辑区提供了 document.execCommand 方法,该方法允许运行命令来操纵可编辑区域的内容。大多数命令影响文档的选择(粗体,斜体等),而其他命令插入新元素(添加链接)或影响整行(缩进)。当使用 contentEditable时,调用 execCommand() 将影响当前活动的可编辑元素。语法如下:

bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
Nach dem Login kopieren

aCommandName: 一个 DOMString ,命令的名称。可用命令列表请参阅 命令 。

aShowDefaultUI: 一个 Boolean, 是否展示用户界面,一般为 false。Mozilla 没有实现。

aValueArgument: 一些命令(例如insertImage)需要额外的参数(insertImage需要提供插入image的url),默认为null。

总之浏览器能把大部分我们想到的富文本编辑器需要的功能都实现了,这里我就不一一演示了。感兴趣的同学可以查看 MDN – document.execCommand。

到这里,我相信你已经可以做出一个像模像样的富文本编辑器了。想想还挺激动的,但是呢,一切都没有结束,浏览器又一次坑了我们。

实战开始,填坑的旅途

就在我们都以为开发如此简单的时候,实际上手却遇到了许多坑。

修正浏览器的默认效果

浏览器提供的富文本效果并不总是好用的,下面介绍几个遇到的问题。

回车换行

当我们在编辑其中输入内容并回车换行继续输入后,可编辑框内容生成的节点和我们预期是不符的。

可以看到最先输入的文字没有被包裹起来,而换行产生的内容,包裹元素是

标签。为了能够让文字被

元素包裹起来。

我们要在初始化的时候,向

默认插入


元素

(
标签用来占位,有内容输入后会自动删除)。这样以后每次回车产生的新内容都会被

元素包裹起来(在可编辑状态下,回车换行产生的新结构会默认拷贝之前的内容,包裹节点,类名等各种内容)。

我们还需要监听 keyUp 事件下 event.keyCode === 8 删除键。当编辑器中内容全被清空后(delete键也会把

标签删除),要重新加入


标签,并把光标定位在里面。

插入 ul 和 ol 位置错误

当我们调用 document.execCommand("insertUnorderedList", false, null) 来插入一个列表的时候,新的列表会被插入

标签中。

为此我们需要每次调用该命令前做一次修正,参考代码如下:

function adjustList() {
    let lists = document.querySelectorAll("ol, ul");
     for (let i = 0; i < lists.length; i++) {
        let ele = lists[i]; // ol
        let parentNode = ele.parentNode;
        if (parentNode.tagName === &#39;P&#39; && parentNode.lastChild ===parentNode.firstChild) {
                parentNode.insertAdjacentElement(&#39;beforebegin&#39;, ele);
                parentNode.remove()
        }
    }
}
Nach dem Login kopieren

这里有个附带的小问题,我试图在

  • 维护这样的编辑器结构(默认是没有

    标签的)。效果在 chrome 下运行很好。但是在 safari 中,回车永远不会产生新的

  • 标签,这样就是去了该有的列表效果。

    插入分割线

    调用 document.execCommand('insertHorizontalRule', false, null); 会插入一个


    标签。然而产生的效果却是这样的:


    光标和


    的效果一致了。为此要判断当前光标是否在
  • 里面,如果是则在
    后面追加一个空的文本节点 #text 不是的话追加


    。然后将光标定位在里面,可用如下方式查找。

    /**
    * 查找父元素
    * @param {String} root
    * @param {String | Array} name
    */
    function findParentByTagName(root, name) {
        let parent = root;
        if (typeof name === "string") {
            name = [name];
        }
        while (name.indexOf(parent.nodeName.toLowerCase()) === -1 &&parent.nodeName !== "BODY" && parent.nodeName !== "HTML") {
            parent = parent.parentNode;
        }
        return parent.nodeName === "BODY" || parent.nodeName === "HTML" ?null : parent;
    },
    Nach dem Login kopieren

    插入链接

    调用 document.execCommand('createLink', false, url); 方法我们可以插入一个 url 链接,但是该方法不支持插入指定文字的链接。同时对已经有链接的位置可以反复插入新的链接。为此我们需要重写此方法。

    function insertLink(url, title) {
        let selection = document.getSelection(),
            range = selection.getRangeAt(0);
        if(range.collapsed) {
            let start = range.startContainer,
                parent = Util.findParentByTagName(start, &#39;a&#39;);
            if(parent) {
                parent.setAttribute(&#39;src&#39;, url);
            }else {
                this.insertHTML(`<a href="${url}">${title}</a>`);
            }
        }else {
            document.execCommand(&#39;createLink&#39;, false, url);
        }
    }
    Nach dem Login kopieren

    设置 h1 ~ h6 标题

    浏览器没有现成的方法,但我们可以借助 document.execCommand('formatBlock', false, tag), 来实现,代码如下:

    function setHeading(heading) {
        let formatTag = heading,
            formatBlock = document.queryCommandValue("formatBlock");
        if (formatBlock.length > 0 && formatBlock.toLowerCase() === formatTag) {
            document.execCommand(&#39;formatBlock&#39;, false, ``);
        } else {
            document.execCommand(&#39;formatBlock&#39;, false, ``);
        }
    }
    Nach dem Login kopieren

    插入定制内容

    当编辑器上传或加载附件的时候,要插入能够展示附件的

    节点卡片到编辑中。这里我们借助 document.execCommand('insertHTML', false, html); 来插入内容。为了防止div被编辑,要设置 contenteditable="false"哦。

    处理 paste 粘贴

    在富文本编辑器中,粘贴效果默认采用如下规则:

    如果是带有格式的文本,则保留格式(格式会被转换成html标签的形式)

    粘贴图文混排的内容,图片可以显示,src 为图片真实地址。

    通过复制图片来进行粘贴的时候,不能粘入内容

    粘贴其他格式内容,不能粘入内容

    为了能够控制粘贴的内容,我们监听 paste 事件。该事件的 event 对象中会包含一个 clipboardData 剪切板对象。我们可以利用该对象的 getData 方法来获得带有格式和不带格式的内容,如下。

    let plainText = event.clipboardData.getData(&#39;text/plain&#39;);  // 无格式文本
    let plainHTML = event.clipboardData.getData(&#39;text/html&#39;);   // 有格式文本
    Nach dem Login kopieren

    之后调用 document.execCommand('insertText', false, plainText); 或 document.execCommand('insertHTML', false, plainHTML; 来重写编辑上的paste效果。

    然而对于规则 3 ,上述方案就无法处理了。这里我们要引入 event.clipboardData.items 。这是一个数组包含了所有剪切板中的内容对象。比如你复制了一张图片来粘贴,那么 event.clipboardData.items 的长度就为2:

    items[0] 为图片的名称,items[0].kind 为 ‘string’, items[0].type 为 ‘text/plain’ 或 ‘text/html’。获取内容方式如下:

    items[0].getAsString(str => {
        // 处理 str 即可
    })
    items[1] 为图片的二进制数据,items[1].kind 为’file’, items[1].type 为图片的格式。想要获取里面的内容,我们就需要创建 FileReader 对象了。示例代码如下:
    let file = items[1].getAsFile();
    // file.size 为文件大小
    let reader = new FileReader();
    reader.onload = function() {
        // reader.result 为文件内容,就可以做上传操作了
    }
    if(/image/.test(item.type)) {
        reader.readAsDataURL(file);   // 读取为 base64 格式
    }
    Nach dem Login kopieren

    处理完图片,那么对于复制粘贴其他格式内容会怎么样呢?在 mac 中,如果你复制一个磁盘文件,event.clipboardData.items 的长度为 2。 items[0] 依然为文件名,然而 items[1] 则为图片了,没错,是文件的缩略图。

    输入法处理

    当使用输入发的时候,有时候会发生一些意想不到的事情。 比如百度输入法可以输入一张本地图片,为此我们需要监听输入法产生的内容做处理。这里通过如下两个事件处理:

    compositionstart: 当浏览器有非直接的文字输入时, compositionstart事件会以同步模式触发

    compositionend: 当浏览器是直接的文字输入时, compositionend会以同步模式触发

    修复移动端的问题

    在移动端,富文本编辑器的问题主要集中在光标和键盘上面。我这里介绍几个比较大的坑。

    自动获取焦点

    如果想让我们的编辑器自动获得焦点,弹出软键盘,可以利用 focus() 方法。然而在 ios 下,死活没有结果。这主要是因为 ios safari 中,为了安全考虑不允许代码获得焦点。只能通过用户交互点击才可以。还好,这一限制可以去除:


    [self.appWebView setKeyboardDisplayRequiresUserAction:NO]


    iOS 下回车换行,滚动条不会自动滚动

    在 iOS 下,当我们回车换行的时候,滚动条并不会随着滚动下去。这样光标就可能被键盘挡住,体验不好。为了解决这一问题,我们就需要监听 selectionchange 事件,触发时,计算每次光标编辑器顶端距离,之后再调用 window.scroll() 即可解决。问题在于我们要如何计算当前光标的位置,如果仅是计算光标所在父元素的位置很有可能出现偏差(多行文本计算不准)。我们可以通过创建一个临时 元素查到光标位置,计算元素的位置即可。代码如下:

    function getCaretYPosition() {
        let sel = window.getSelection(),
            range = sel.getRangeAt(0);
        let span = document.createElement(&#39;span&#39;);
        range.collapse(false);
        range.insertNode(span);
        var topPosition = span.offsetTop;
        span.parentNode.removeChild(span);
        return topPosition;
    }
    Nach dem Login kopieren

    正当我开心的时候,安卓端反应,编辑器越编辑越卡。什么鬼?我在 chrome 上线检查了一下,发现 selectionchange 函数一直在运行,不管有没有操作。

    在逐一排查的时候发现了这么一个事实。range.insertNode 函数同样触发 selectionchange 事件。这样就形成了一个死循环。这个死循环在 safari 中就不会产生,只出现在 safari 中,为此我们就需要加上浏览器类型判断了。

    键盘弹起遮挡输入部分

    网上对于这个问题主要的方案就是,设置定时器。局限与前端,确实只能这采用这样笨笨的解决。最后我们让 iOS 同学在键盘弹出的时候,将 webview 高度减去软键盘高度就解决了。

    CGFloat webviewY = 64.0 + self.noteSourceView.height;
    self.appWebView.frame = CGRectMake(0, webviewY, BDScreenWidth,BDScreenHeight - webviewY - height);
    Nach dem Login kopieren

    插入图片失败

    在移动端,通过调用 jsbridge 来唤起相册选择图片。之后调用 insertImage 函数来向编辑器插入图片。然而,插入图片一直失败。最后发现是因为早 safari 下,如果编辑器失去了焦点,那么 selection 和 range 对象将销毁。因此调用 insertImage 时,并不能获得光标所在位置,因此失败。为此需要增加,backupRange() 和 restoreRange() 函数。当页面失去焦点的时候记录 range 信息,插入图片前恢复 range 信息。

    backupRange() {
        let selection = window.getSelection();
        let range = selection.getRangeAt(0);
        this.currentSelection = {
            "startContainer": range.startContainer,
            "startOffset": range.startOffset,
            "endContainer": range.endContainer,
            "endOffset": range.endOffset
        }
    }
    restoreRange() {
        if (this.currentSelection) {
            let selection = window.getSelection();
                selection.removeAllRanges();
            let range = document.createRange();
            range.setStart(this.currentSelection.startContainer,this.currentSelection.startOffset);
            range.setEnd(this.currentSelection.endContainer,this.currentSelection.endOffset);
            // 向选区中添加一个区域
            selection.addRange(range);
        }
    }
    Nach dem Login kopieren

    在 chrome 中,失去焦点并不会清除 seleciton 对象和 range 对象,这样我们轻轻松松一个 focus() 就搞定了。

    重要问题就这么多,限于篇幅限制其他的问题省略了。总体来说,填坑花了开发的大部分时间。

    其他功能

    基础功能修修补补以后,实际项目中有可能遇到一些其他的需求,比如当前光标所在文字内容状态啊,图片拖拽放大啊,待办列表功能,附件卡片等功能啊,markdown切换等等。在了解了js 富文本的种种坑之后,range 对象的操作之后,相信这些问题你都可以轻松解决。这里最后提几个做扩展功能时候遇到的有去的问题。

    回车换行带格式

    前面已经说过了,富文本编辑器的机制就是这样,当你回车换行的时候新产生的内容和之前的格式一模一样。如果我们利用 .card 类来定义了一个卡片内容,那么换行产生的新的段落都将含有 .card 类且结构也是直接 copy 过来的。我们想要屏蔽这种机制,于是尝试在 keydown 的阶段做处理(如果在 keyup 阶段处理用户体验不好)。然而,并没有什么用,因为用户自定义的 keydown 事件要在 浏览器富文本的默认 keydown 事件之前触发,这样你就做不了任何处理。

    为此我们为这类特殊的个体都添加一个 property 属性,添加在 property 上的内容是不会被copy下来的。这样以后就可以区分出来了,从而做对应的处理。

    获取当前光标所在处样式

    这里主要是考虑 下划线,删除线之类的样式,这些样式都是用标签类描述的,所以要遍历标签层级。直接上代码:

    function getCaretStyle() {
        let selection = window.getSelection(),
            range = selection.getRangeAt(0);
            aimEle = range.commonAncestorContainer,
            tempEle = null;
        let tags = ["U", "I", "B", "STRIKE"],
            result = [];
        if(aimEle.nodeType === 3) {
            aimEle = aimEle.parentNode;
        }
        tempEle = aimEle;
        while(block.indexOf(tempEle.nodeName.toLowerCase()) === -1) {
            if(tags.indexOf(tempEle.nodeName) !== -1) {
                result.push(tempEle.nodeName);
            }
            tempEle = tempEle.parentNode;
        }
        let viewStyle = {
            "italic": result.indexOf("I") !== -1 ? true : false,
            "underline": result.indexOf("U") !== -1 ? true : false,
            "bold": result.indexOf("B") !== -1 ? true : false,
            "strike": result.indexOf("STRIKE") !== -1 ? true : false
        }
        let styles = window.getComputedStyle(aimEle, null);
        viewStyle.fontSize = styles["fontSize"],
        viewStyle.color = styles["color"],
        viewStyle.fontWeight = styles["fontWeight"],
        viewStyle.fontStyle = styles["fontStyle"],
        viewStyle.textDecoration = styles["textDecoration"];
        viewStyle.isH1 = Util.findParentByTagName(aimEle, "h1") ? true : false;
        viewStyle.isH2 = Util.findParentByTagName(aimEle, "h2") ? true : false;
        viewStyle.isP = Util.findParentByTagName(aimEle, "p") ? true : false;
        viewStyle.isUl = Util.findParentByTagName(aimEle, "ul") ? true : false;
        viewStyle.isOl = Util.findParentByTagName(aimEle, "ol") ? true : false;
        return viewStyle;
    }
    Nach dem Login kopieren

    以上就是我做富文本编辑器的思路,也希望给大家带来帮助。

    Das obige ist der detaillierte Inhalt vonImplementieren Sie einen Rich-Text-Editor mit Javascript. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

  • Verwandte Etiketten:
    Quelle:php.cn
    Erklärung dieser Website
    Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
    Beliebte Tutorials
    Mehr>
    Neueste Downloads
    Mehr>
    Web-Effekte
    Quellcode der Website
    Website-Materialien
    Frontend-Vorlage
    Über uns Haftungsausschluss Sitemap
    Chinesische PHP-Website:Online-PHP-Schulung für das Gemeinwohl,Helfen Sie PHP-Lernenden, sich schnell weiterzuentwickeln!