Twitter如何实现其推文框?

tre*_*ver 10 html javascript twitter range selection

我正在尝试实现Twitter的推文框,特别是:

  • 当总长度超过140个字符时,自动突出显示红色背景中的文本.
  • 自动以蓝色突出显示链接,提及和主题标签.

这些应该在用户输入时自动发生.

通过我在Twitter上看到的语义标记,看起来他们正在使用contentEditablediv.每当检测到提及/#标签/链接时,或者超过140个字符的长度时,都会修改DOM内部:

<div aria-labelledby="tweet-box-mini-home-profile-label" id="tweet-box-mini-home-profile" class="tweet-box rich-editor  notie" contenteditable="true" spellcheck="true" role="textbox" aria-multiline="true" dir="ltr" aria-autocomplete="list" aria-expanded="false" aria-owns="typeahead-dropdown-6">
    <div>hello world <a class="twitter-atreply pretty-link" href="/testMention" role="presentation"><s>@</s>testMention</a> text <a href="/search?q=%23helloWorld" class="twitter-hashtag pretty-link" role="presentation"><s>#</s>helloWorld</a> text <a href="http://www.google.com" class="twitter-timeline-link" role="presentation">http://www.google.com</a> text text text text text text text text text text text text text text <em>overflow red text here</em>
    </div>
</div>
Run Code Online (Sandbox Code Playgroud)

到目前为止我做了什么

目前,我正在使用contentEditable字段.它会触发一个onChange/onInput解析文本的函数.通过regexr检查它是否有用户名/链接/主题标签,并用相应的标签替换它们(我现在只使用一个简单的<i>标签来包含用户名和主题标签). 我遇到的问题是因为我正在更改/修改contentEditable的DOM,所以插入符号光标位置会丢失. 我查看了这个线程在HTML中选择后保持范围对象的更改并且只有在未更改DOM时它才能正常工作(在我的情况下,它包含带有<i>标记的标记/主题标签,带有<a>标记的链接以及带有<b>标记的溢出).

小提琴:http://jsfiddle.net/4Lsqjkjb/

有没有人有解决这个问题的替代解决方案/方法?也许我不应该使用contentEditable?或者某种方式不直接修改contentEditable字段的DOM以保持插入位置?任何帮助,将不胜感激!我尝试window.getSelection()在DOM更改之前使用并保存它,但在应用DOM更改后似乎总是重置.

Jer*_*art 6

这不是根据您的规范构建应用程序部分的源代码的直接答案。

真的不是一件容易的事情。

你是对的 - 解决这个问题的方法是使用contenteditable="true"容器。但恐怕事情会变得复杂得多

输入 DraftJS - 富文本编辑的 javascript 解决方案,可为您完成大部分繁重工作。

我下面的两个解决方案都需要使用 React 和 DraftJS


即插即用解决方案:

阵营+ DraftJS +别人的“插件”的解决方案是已经在这里。您可以查看draft-js-plugins.com。这是 Github 源代码

就我个人而言,我不会走这条路。我宁愿研究他们的 GitHub 源代码并实现我自己的 DraftJS entities。因为我认为 React-JS-Plugins 感觉有点笨重和超重,仅用于链接和提及。另外,你从哪里“提到”?你自己的应用程序?推特?这样做可以让您控制在何处获取所谓的“提及”。


DIY解决方案:

我发现的最好方法是entities在基于 DraftJS 的富文本编辑器之上构建您自己的工作集。

这当然也要求您使用React

请原谅我没有真正构建一套完整的工作代码。我有兴趣为我在 Meteor 中使用前端构建的应用程序做类似的事情。所以这对我来说真的很有意义,因为我只会再添加一个库 (DraftJS),其余的将是我的自定义entities编码。


Hus*_*ard 6

我所做的是一种黑客行为,但作为一种变通解决方案效果很好。

我有一个简单的 textarea(不满意,因为接下来会发生什么,我将在下面解释)和一个绝对位于 textarea 后面的 div。使用 javascript,我将内容从 textarea 复制到 div,同时将其拆分为 140 个字符并将所有额外字符放入<em />标签中。

好吧,它有点复杂,因为计算推文长度的 twitter 算法是不同的(由于 t.co url 缩短,链接不算作它们的实际价值)。确切的方法可以在官方twitter/twitter-txt 存储库中找到

一步一步的代码。

将一个简单的 textarea 包裹到一个 div 中以简化 css:

<div class="tweet-composer">
  <textarea class="editor-textarea js-keeper-editor">This is some text that will be highlight when longer than 20 characters. Like twitter. Type something...</textarea>
  <div class="js-keeper-placeholder-back"></div>
</div>
Run Code Online (Sandbox Code Playgroud)

CSS 只是将 textarea 和 div 放在彼此的正上方并突出显示文本。

.tweet-composer {
  position: relative;
  z-index: 1;
}

.js-keeper-editor,
.js-keeper-placeholder-back {
  background: transparent;
  border: 1px solid #eee;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 14px; /* Same font for both. */
  margin: auto;
  min-height: 200px;
  outline: none;
  padding: 10px;
  width: 100%;
}

.js-keeper-placeholder-back {
  bottom: 0;
  color: transparent;
  left: 0;
  position: absolute;
  top: 0;
  white-space: pre-wrap;
  width: 100%;
  word-wrap: break-word;
  z-index: -1;
}

.js-keeper-placeholder-back em {
  background: #fcc !important;
}
Run Code Online (Sandbox Code Playgroud)

现在是有趣的部分,这是使用 jQuery 实现的,但这不是重要的事情。

if (0 > remainingLength) {
  // Split value if greater than 
  var allowedValuePart = currentValue.slice(0, realLength),
      refusedValuePart = currentValue.slice(realLength)
  ;

  // Fill the hidden div.
  $placeholderBacker.html(allowedValuePart + '<em>' + refusedValuePart + '</em>');
} else {
  $placeholderBacker.html('');
}
Run Code Online (Sandbox Code Playgroud)

在更改时添加一些事件处理程序,并准备好公共文档,您就完成了。请参阅下面的代码笔链接。

请注意,在加载页面时,放置在后面的 div 也可以使用 js 创建:

// Create a pseudo-element that will be hidden behind the placeholder.
var $placeholderBacker = $('<div class="js-keeper-placeholder-back"></div>');
$placeholderBacker.insertAfter($textarea);
Run Code Online (Sandbox Code Playgroud)

完整示例

请参阅此处的工作示例: http : //codepen.io/hussard/pen/EZvaBZ


Aar*_*ron 1

事实证明,这确实不是一件容易做到的事情。过去几天我一直在努力解决这个问题,但我还没有找到解决方案。

\n\n

目前最好的嵌入式解决方案是At.js 库,它仍在维护中,但绝对不完美。其中一个示例展示了如何进行文本突出显示。

\n\n

这个问题最烦人的部分是 Twitter 有一个漂亮的解决方案,看起来效果非常好,就在我们面前。我花了一些时间研究他们实现“推文框”的方式,这绝对不是小事。看起来他们几乎所有事情都是手动完成的,包括模拟撤消/重做功能、拦截复制/粘贴、为 IE/W3C 提供自定义代码、自定义编码 Mac/PC 等等。他们使用 contenteditable div,由于浏览器实现的差异,这本身就有问题。实际上,这非常令人印象深刻。

\n\n

这是最相关的(不幸的是,被混淆了)代码,取自 Twitter 的启动 JavaScript 文件(通过检查登录的 Twitter 主页的标头找到)。我不想直接复制并粘贴该链接,以防它针对我的 Twitter 帐户进行个性化设置。

\n\n
define("app/utils/html_text", ["module", "require", "exports"], function(module, require, exports) {\n    function isTextNode(a) {\n        return a.nodeType == 3 || a.nodeType == 4\n    }\n\n    function isElementNode(a) {\n        return a.nodeType == 1\n    }\n\n    function isBrNode(a) {\n        return isElementNode(a) && a.nodeName.toLowerCase() == "br"\n    }\n\n    function isOutsideContainer(a, b) {\n        while (a !== b) {\n            if (!a) return !0;\n            a = a.parentNode\n        }\n    }\n    var useW3CRange = window.getSelection,\n        useMsftTextRange = !useW3CRange && document.selection,\n        useIeHtmlFix = navigator.appName == "Microsoft Internet Explorer",\n        NBSP_REGEX = /[\\xa0\\n\\t]/g,\n        CRLF_REGEX = /\\r\\n/g,\n        LINES_REGEX = /(.*?)\\n/g,\n        SP_LEADING_OR_FOLLOWING_CLOSE_TAG_OR_PRECEDING_A_SP_REGEX = /^ |(<\\/[^>]+>) | (?= )/g,\n        SP_LEADING_OR_TRAILING_OR_FOLLOWING_A_SP_REGEX = /^ | $|( ) /g,\n        MAX_OFFSET = Number.MAX_VALUE,\n        htmlText = function(a, b) {\n            function c(a, c) {\n                function h(a) {\n                    var i = d.length;\n                    if (isTextNode(a)) {\n                        var j = a.nodeValue.replace(NBSP_REGEX, " "),\n                            k = j.length;\n                        k && (d += j, e = !0), c(a, !0, 0, i, i + k)\n                    } else if (isElementNode(a)) {\n                        c(a, !1, 0, i, i);\n                        if (isBrNode(a)) a == f ? g = !0 : (d += "\\n", e = !1);\n                        else {\n                            var l = a.currentStyle || window.getComputedStyle(a, ""),\n                                m = l.display == "block";\n                            m && b.msie && (e = !0);\n                            for (var n = a.firstChild, o = 1; n; n = n.nextSibling, o++) {\n                                h(n);\n                                if (g) return;\n                                i = d.length, c(a, !1, o, i, i)\n                            }\n                            g || a == f ? g = !0 : m && e && (d += "\\n", e = !1)\n                        }\n                    }\n                }\n                var d = "",\n                    e, f, g;\n                for (var i = a; i && isElementNode(i); i = i.lastChild) f = i;\n                return h(a), d\n            }\n\n            function d(a, b) {\n                var d = null,\n                    e = b.length - 1;\n                if (useW3CRange) {\n                    var f = b.map(function() {\n                            return {}\n                        }),\n                        g;\n                    c(a, function(a, c, d, h, i) {\n                        g || f.forEach(function(f, j) {\n                            var k = b[j];\n                            h <= k && !isBrNode(a) && (f.node = a, f.offset = c ? Math.min(k, i) - h : d, g = c && j == e && i >= k)\n                        })\n                    }), f[0].node && f[e].node && (d = document.createRange(), d.setStart(f[0].node, f[0].offset), d.setEnd(f[e].node, f[e].offset))\n                } else if (useMsftTextRange) {\n                    var h = document.body.createTextRange();\n                    h.moveToElementText(a), d = h.duplicate();\n                    if (b[0] == MAX_OFFSET) d.setEndPoint("StartToEnd", h);\n                    else {\n                        d.move("character", b[0]);\n                        var i = e && b[1] - b[0];\n                        i > 0 && d.moveEnd("character", i), h.inRange(d) || d.setEndPoint("EndToEnd", h)\n                    }\n                }\n                return d\n            }\n\n            function e() {\n                return document.body.contains(a)\n            }\n\n            function f(b) {\n                a.innerHTML = b;\n                if (useIeHtmlFix)\n                    for (var c = a.firstChild; c; c = c.nextSibling) isElementNode(c) && c.nodeName.toLowerCase() == "p" && c.innerHTML == "" && (c.innerText = "")\n            }\n\n            function g(a, b) {\n                return a.map(function(a) {\n                    return Math.min(a, b.length)\n                })\n            }\n\n            function h() {\n                var b = getSelection();\n                if (b.rangeCount !== 1) return null;\n                var d = b.getRangeAt(0);\n                if (isOutsideContainer(d.commonAncestorContainer, a)) return null;\n                var e = [{\n                    node: d.startContainer,\n                    offset: d.startOffset\n                }];\n                d.collapsed || e.push({\n                    node: d.endContainer,\n                    offset: d.endOffset\n                });\n                var f = e.map(function() {\n                        return MAX_OFFSET\n                    }),\n                    h = c(a, function(a, b, c, d) {\n                        e.forEach(function(e, g) {\n                            f[g] == MAX_OFFSET && a == e.node && (b || c == e.offset) && (f[g] = d + (b ? e.offset : 0))\n                        })\n                    });\n                return g(f, h)\n            }\n\n            function i() {\n                var b = document.selection.createRange();\n                if (isOutsideContainer(b.parentElement(), a)) return null;\n                var d = ["Start"];\n                b.compareEndPoints("StartToEnd", b) && d.push("End");\n                var e = d.map(function() {\n                        return MAX_OFFSET\n                    }),\n                    f = document.body.createTextRange(),\n                    h = c(a, function(c, g, h, i) {\n                        function j(a, c) {\n                            if (e[c] < MAX_OFFSET) return;\n                            var d = f.compareEndPoints("StartTo" + a, b);\n                            if (d > 0) return;\n                            var g = f.compareEndPoints("EndTo" + a, b);\n                            if (g < 0) return;\n                            var h = f.duplicate();\n                            h.setEndPoint("EndTo" + a, b), e[c] = i + h.text.length, c && !g && e[c]++\n                        }!g && !h && c != a && (f.moveToElementText(c), d.forEach(j))\n                    });\n                return g(e, h)\n            }\n            return {\n                getHtml: function() {\n                    if (useIeHtmlFix) {\n                        var b = "",\n                            c = document.createElement("div");\n                        for (var d = a.firstChild; d; d = d.nextSibling) isTextNode(d) ? (c.innerText = d.nodeValue, b += c.innerHTML) : b += d.outerHTML.replace(CRLF_REGEX, "");\n                        return b\n                    }\n                    return a.innerHTML\n                },\n                setHtml: function(a) {\n                    f(a)\n                },\n                getText: function() {\n                    return c(a, function() {})\n                },\n                setTextWithMarkup: function(a) {\n                    f((a + "\\n").replace(LINES_REGEX, function(a, c) {\n                        return b.mozilla || b.msie ? (c = c.replace(SP_LEADING_OR_FOLLOWING_CLOSE_TAG_OR_PRECEDING_A_SP_REGEX, "$1&nbsp;"), b.mozilla ? c + "<BR>" : "<P>" + c + "</P>") : (c = (c || "<br>").replace(SP_LEADING_OR_TRAILING_OR_FOLLOWING_A_SP_REGEX, "$1&nbsp;"), b.opera ? "<p>" + c + "</p>" : "<div>" + c + "</div>")\n                    }))\n                },\n                getSelectionOffsets: function() {\n                    var a = null;\n                    return e() && (useW3CRange ? a = h() : useMsftTextRange && (a = i())), a\n                },\n                setSelectionOffsets: function(b) {\n                    if (b && e()) {\n                        var c = d(a, b);\n                        if (c)\n                            if (useW3CRange) {\n                                var f = window.getSelection();\n                                f.removeAllRanges(), f.addRange(c)\n                            } else useMsftTextRange && c.select()\n                    }\n                },\n                emphasizeText: function(b) {\n                    var f = [];\n                    b && b.length > 1 && e() && (c(a, function(a, c, d, e, g) {\n                        if (c) {\n                            var h = Math.max(e, b[0]),\n                                i = Math.min(g, b[1]);\n                            i > h && f.push([h, i])\n                        }\n                    }), f.forEach(function(b) {\n                        var c = d(a, b);\n                        c && (useW3CRange ? c.surroundContents(document.createElement("em")) : useMsftTextRange && c.execCommand("italic", !1, null))\n                    }))\n                }\n            }\n        };\n    module.exports = htmlText\n});\n\n\n\n\n\n\n\n\ndefine("app/utils/tweet_helper", ["module", "require", "exports", "lib/twitter-text", "core/utils", "app/data/user_info"], function(module, require, exports) {\n    var twitterText = require("lib/twitter-text"),\n        utils = require("core/utils"),\n        userInfo = require("app/data/user_info"),\n        VALID_PROTOCOL_PREFIX_REGEX = /^https?:\\/\\//i,\n        tweetHelper = {\n            extractMentionsForReply: function(a, b) {\n                var c = a.attr("data-screen-name"),\n                    d = a.attr("data-retweeter"),\n                    e = a.attr("data-mentions") ? a.attr("data-mentions").split(" ") : [],\n                    f = a.attr("data-tagged") ? a.attr("data-tagged").split(" ") : [];\n                e = e.concat(f);\n                var g = [c, b, d];\n                return e = e.filter(function(a) {\n                    return g.indexOf(a) < 0\n                }), d && d != c && d != b && e.unshift(d), (!e.length || c != b) && e.unshift(c), e\n            },\n            linkify: function(a, b) {\n                return b = utils.merge({\n                    hashtagClass: "twitter-hashtag pretty-link",\n                    hashtagUrlBase: "/search?q=%23",\n                    symbolTag: "s",\n                    textWithSymbolTag: "b",\n                    cashtagClass: "twitter-cashtag pretty-link",\n                    cashtagUrlBase: "/search?q=%24",\n                    usernameClass: "twitter-atreply pretty-link",\n                    usernameUrlBase: "/",\n                    usernameIncludeSymbol: !0,\n                    listClass: "twitter-listname pretty-link",\n                    urlClass: "twitter-timeline-link",\n                    urlTarget: "_blank",\n                    suppressNoFollow: !0,\n                    htmlEscapeNonEntities: !0\n                }, b || {}), twitterText.autoLinkEntities(a, twitterText.extractEntitiesWithIndices(a), b)\n            }\n        };\n    module.exports = tweetHelper\n});\n\n\n\n\n\n\n\n\n\n\ndefine("app/ui/compose/with_rich_editor", ["module", "require", "exports", "app/utils/file", "app/utils/html_text", "app/utils/tweet_helper", "lib/twitter-text"], function(module, require, exports) {\n    function withRichEditor() {\n        this.defaultAttrs({\n            richSelector: "div.rich-editor",\n            linksSelector: "a",\n            normalizerSelector: "div.rich-normalizer",\n            $browser: $.browser\n        }), this.linkify = function(a) {\n            var b = {\n                urlTarget: null,\n                textWithSymbolTag: RENDER_URLS_AS_PRETTY_LINKS ? "b" : "",\n                linkAttributeBlock: function(a, b) {\n                    var c = a.screenName || a.url;\n                    c && (this.urlAndMentionsCharCount += c.length + 2), delete b.title, delete b["data-screen-name"], b.dir = a.hashtag && this.shouldBeRTL(a.hashtag, 0) ? "rtl" : "ltr", b.role = "presentation"\n                }.bind(this)\n            };\n            return this.urlAndMentionsCharCount = 0, tweetHelper.linkify(a, b)\n        }, this.around("setSelection", function(a, b) {\n            b && this.setSelectionIfFocused(b)\n        }), this.around("setCursorPosition", function(a, b) {\n            b === undefined && (b = this.attr.cursorPosition), b === undefined && (b = MAX_OFFSET), this.setSelectionIfFocused([b])\n        }), this.around("detectUpdatedText", function(a, b, c) {\n            var d = this.htmlRich.getHtml(),\n                e = this.htmlRich.getSelectionOffsets() || [MAX_OFFSET],\n                f = c !== undefined;\n            if (d === this.prevHtml && e[0] === this.prevSelectionOffset && !b && !f) return;\n            var g = f ? c : this.htmlRich.getText(),\n                h = g.replace(INVALID_CHARS_REGEX, "");\n            (f || !(!d && !this.hasFocus() || this.$text.attr("data-in-composition"))) && this.reformatHtml(h, d, e, f);\n            if (b || this.cleanedText != h || this.prevSelectionOffset != e[0]) this.prevSelectionOffset = e[0], this.updateCleanedTextAndOffset(h, e[0])\n        }), this.reformatHtml = function(a, b, c, d) {\n            this.htmlNormalizer.setTextWithMarkup(this.linkify(a)), this.interceptDataImageInContent();\n            var e = this.shouldBeRTL(a, this.urlAndMentionsCharCount);\n            this.$text.attr("dir", e ? "rtl" : "ltr"), this.$normalizer.find(e ? "[dir=rtl]" : "[dir=ltr]").removeAttr("dir"), RENDER_URLS_AS_PRETTY_LINKS && this.$normalizer.find(".twitter-timeline-link").wrapInner("<b>").addClass("pretty-link");\n            var f = this.getMaxLengthOffset(a);\n            f >= 0 && (this.htmlNormalizer.emphasizeText([f, MAX_OFFSET]), this.$normalizer.find("em").each(function() {\n                this.innerHTML = this.innerHTML.replace(TRAILING_SINGLE_SPACE_REGEX, "\xc3\x82 ")\n            }));\n            var g = this.htmlNormalizer.getHtml();\n            if (g !== b) {\n                var h = d && !this.isFocusing && this.hasFocus();\n                h && this.$text.addClass("fake-focus").blur(), this.htmlRich.setHtml(g), h && this.$text.focus().removeClass("fake-focus"), this.setSelectionIfFocused(c)\n            }\n            this.prevHtml = g\n        }, this.interceptDataImageInContent = function() {\n            if (!this.triggerGotImageData) return;\n            this.$text.find("img").filter(function(a, b) {\n                return b.src.match(/^data:/)\n            }).first().each(function(a, b) {\n                var c = file.getBlobFromDataUri(b.src);\n                file.getFileData("pasted.png", c).then(this.triggerGotImageData.bind(this))\n            }.bind(this))\n        }, this.getMaxLengthOffset = function(a) {\n            var b = this.getLength(a),\n                c = this.attr.maxLength;\n            if (b <= c) return -1;\n            c += twitterText.getUnicodeTextLength(a) - b;\n            var d = [{\n                indices: [c, c]\n            }];\n            return twitterText.modifyIndicesFromUnicodeToUTF16(a, d), d[0].indices[0]\n        }, this.setSelectionIfFocused = function(a) {\n            this.hasFocus() ? (this.previousSelection = null, this.htmlRich.setSelectionOffsets(a)) : this.previousSelection = a\n        }, this.selectPrevCharOnBackspace = function(a) {\n            if (a.which == 8 && !a.ctrlKey) {\n                var b = this.htmlRich.getSelectionOffsets();\n                b && b[0] != MAX_OFFSET && b.length == 1 && (b[0] ? this.setSelectionIfFocused([b[0] - 1, b[0]]) : this.stopEvent(a))\n            }\n        }, this.emulateCommandArrow = function(a) {\n            if (a.metaKey && !a.shiftKey && (a.which == 37 || a.which == 39)) {\n                var b = a.which == 37;\n                this.htmlRich.setSelectionOffsets([b ? 0 : MAX_OFFSET]), this.$text.scrollTop(b ? 0 : this.$text[0].scrollHeight), this.stopEvent(a)\n            }\n        }, this.stopEvent = function(a) {\n            a.preventDefault(), a.stopPropagation()\n        }, this.saveUndoStateDeferred = function(a) {\n            if (a.type == "mousemove" && a.which != 1) return;\n            setTimeout(function() {\n                this.detectUpdatedText(), this.saveUndoState()\n            }.bind(this), 0)\n        }, this.clearUndoState = function() {\n            this.undoHistory = [], this.undoIndex = -1\n        }, this.saveUndoState = function() {\n            var a = this.htmlRich.getText(),\n                b = this.htmlRich.getSelectionOffsets() || [a.length],\n                c = this.undoHistory,\n                d = c[this.undoIndex];\n            !d || d[0] !== a ? c.splice(++this.undoIndex, c.length, [a, b]) : d && (d[1] = b)\n        }, this.isUndoKey = function(a) {\n            return this.isMac ? a.which == 90 && a.metaKey && !a.shiftKey && !a.ctrlKey && !a.altKey : a.which == 90 && a.ctrlKey && !a.shiftKey && !a.altKey\n        }, this.emulateUndo = function(a) {\n            this.isUndoKey(a) && (this.stopEvent(a), this.saveUndoState(), this.undoIndex > 0 && this.setUndoState(this.undoHistory[--this.undoIndex]))\n        }, this.isRedoKey = function(a) {\n            return this.isMac ? a.which == 90 && a.metaKey && a.shiftKey && !a.ctrlKey && !a.altKey : this.isWin ? a.which == 89 && a.ctrlKey && !a.shiftKey && !a.altKey : a.which == 90 && a.shiftKey && a.ctrlKey && !a.altKey\n        }, this.emulateRedo = function(a) {\n            var b = this.undoHistory,\n                c = this.undoIndex;\n            c < b.length - 1 && this.htmlRich.getText() !== b[c][0] && b.splice(c + 1, b.length), this.isRedoKey(a) && (this.stopEvent(a), c < b.length - 1 && this.setUndoState(b[++this.undoIndex]))\n        }, this.setUndoState = function(a) {\n            this.detectUpdatedText(!1, a[0]), this.htmlRich.setSelectionOffsets(a[1]), this.trigger("uiHideAutocomplete")\n        }, this.undoStateAfterCursorMovement = function(a) {\n            a.which >= 33 && a.which <= 40 && this.saveUndoStateDeferred(a)\n        }, this.handleKeyDown = function(a) {\n            this.isIE && this.selectPrevCharOnBackspace(a), this.attr.$browser.mozilla && this.emulateCommandArrow(a), this.undoStateAfterCursorMovement(a), this.emulateUndo(a), this.emulateRedo(a)\n        }, this.interceptPaste = function(a) {\n            if (a.originalEvent && a.originalEvent.clipboardData) {\n                var b = a.originalEvent.clipboardData;\n                (this.interceptImagePaste(b) || this.interceptTextPaste(b)) && a.preventDefault()\n            }\n        }, this.interceptImagePaste = function(a) {\n