粘贴垃圾对 contentEditable 说; “嘿!我真的很想成为你的一部分”,contentEditable 回复说; “没那么快,你!首先我们得把你冲洗干净!”。因此开始了一个关于如何让 contentEditable 接受一个好的 ol' 粘贴的故事,解析该粘贴以查找我们可能不想要的任何内容,将解析结果放入 contentEditable 中的正确位置,并在粘贴之后放置插入符号。听起来很容易,对吧?对。默认情况下, contentEditable 接受您想要提供的任何内容。如果您从任何也有标记和样式的地方(如 Word 文档)复制文本,然后将其粘贴到 contentEditable 中,它也很乐意采用所有标记和样式。但是,如果您像我一样构建内容编辑器,这并不是一个很好的用户体验,因此最好的解决方案是解析该粘贴并删除您可能不想要的任何内容 - 在我的情况下是删除所有样式并只允许一定的加价。好的,让我们创建一个简单的 contentEditable 来监听 Paste 事件。我将在 ClojureScript 中执行此操作,因为它是我最喜欢的语言,使用 Reagent 作为 React 的优点,因为这是一个 React 应用程序,但所有这些也适用于好的 ol' 常规 JS 和 React.js。您难道不喜欢在 ClojureScript 中编写 React 组件只需编写如此少的代码吗?我肯定会,这完全不是(眨眼)我说你应该尝试 ClojureScript 的方式。无论如何,让我们创建粘贴!功能一样。哦,射击,它是空的!是的,我想在这里停下来,因为我不知道,现在有一个标准的剪贴板 API,您应该使用它来获取粘贴的用户内容 - 但它带有一个问题 - 一旦您尝试使用它,浏览器就会要求用户授予您的页面读取剪贴板数据的权限,我发现这对于能够将文本粘贴到输入中这样简单的事情来说不是很用户友好,因为当您使用默认行为将文本粘贴到输入中时它不会询问,但无论如何,ce'st la vie。 (defn on-paste! [event] (.then (.readText (.-clipboard js/navigator)) (fn [clip] ;; `clip` 包含粘贴的内容 )))) 现在剪辑是实际的粘贴,连同它所有可怕的格式和样式,所以我继续使用 sanitize-html NPM 包来清理它(我确实想在某一时刻构建一个原生的 Clojure 版本,但现在这只是膨胀!)。所以,有了那个包,贴上!函数看起来像这样:
(defn on-paste! [event] (.then (.readText (.-clipboard js/navigator)) (fn [clip] (let [pasted-content (parse-html clip)] ;; 用`pasted- content` 在这里 ))))) (ns your-app (:require ["sanitize-html" :as sanitize-html]))(defn parse-html [html] (sanitize-html html (clj->js {: allowedTags ["b" "strong" "i" "em" "a" "u"] :allowedAttributes {"a" ["href"]}})))),正如我确定你可以说的,只允许标签 b, strong, i, em, a, u and 只允许 a 标签上的属性,并且仅当该属性是 href 时。很酷吧?我肯定是这么认为的。哇!那个押韵!说不定我也可以在嘻哈事业上有所作为哈哈!是的,现在我们有了粘贴并且我们已经成功地清除了它可能有的任何垃圾,我们必须以某种方式将该粘贴放入我们的 contentEditable 中。我们怎么做?我们是否只是将它插入到 DOM 元素中?这不是很 React-y 现在是它。如果我们为内容创建一个本地状态并对其进行修改会怎样?这听起来好多了,实际上。让我们回到我们的 React 组件并添加更改,使其看起来像这样: (ns your-app (:require [reagent.core :as r])) (defn contentEditable [] (let [content (r/ atom "")] (fn [] [:div {:contentEditable true :on-paste #(on-paste! content %) :on-input #(reset! content (.-innerHTML (.-target %))) :dangerouslySetInnerHTML {:__html @content}}]))) 如您所见,我们创建了一个 Reagent atom 并将其设置为空字符串,然后我们使用 :dangerouslySetInnerHTML 属性将其取消引用到 contentEditable 内容中。在每次更改内容时(:on-input 事件),我们更新内容原子,以便它始终与 contentEditable 中的实际内容保持同步,最后注意 on-paste!调用 - 我们现在也将内容传递给它,以便粘贴!函数会知道当前的内容是什么。
所以现在我们需要做的就是将内容粘贴到正确的位置,就是更改 on-paste!功能来知道粘贴发生时插入符号的位置并将粘贴插入那里。上贴!函数将如下所示: (defn on-paste! [content event] (.then (.readText (.-clipboard js/navigator)) (fn [clip] (let [pasted-content (parse-html clip) selection (.getSelection js/window) offset (.-anchorOffset selection) new-content (string->string @content pasteed-content offset)] (reset! content new-content)))))) 所以看看这个,我们得到通过 (.getSelection js/window) 获取当前选择,然后我们可以使用 (.-anchorOffset selection) 获取插入符号偏移量,而该偏移量是关键!这是粘贴时插入符号所在文本开头的基于索引的字符数,因此这也是我们需要放置粘贴内容的位置。我为此创建了一个名为 string->string 的辅助函数,它看起来像这样: (defn string->string [string insert-string index] (let [split-beginning (subs string 0 index) split-end (subs字符串索引)] (str split-beginning insert-string split-end))) 它将原始内容作为字符串,然后是您想要插入的内容作为插入字符串,最后是您想要插入的索引新内容。然后它会返回最终的字符串。正如你在粘贴的末尾看到的那样!我们称为 reset! 的函数基本上只是用新内容覆盖内容原子,提示重新渲染组件,因此现在 contentEditable 具有粘贴的内容,并根据需要在正确的位置删除了所有垃圾。您可能已经注意到的一件事是,在粘贴内容时,插入符号本身最终会出现在错误的位置 - 或者更确切地说是正确的位置,也就是说插入符号将保留在原来的位置,但您可能希望它最终只是在粘贴内容之后,因为它通常是这样工作的。发生这种情况是因为虽然 contentEditable 的内容发生了变化,但插入符号的位置没有发生变化,所以我们必须自己改变它。
值得庆幸的是,这比人们想象的要容易,我们只需要获取当前的插入符偏移量并将粘贴内容的字符数添加到其中即可。假设你的插入符号在偏移 10 处,粘贴的字符串长度为 7,那么自然我们想要 10 + 7,这意味着插入符号将是第 18 个字符。为此,我们必须将我们的组件变成类组件,因为这就是您在 Reagent 中获取生命周期事件的方式。为什么?因为我们需要能够在组件呈现之后放置插入符,而不是之前,因为我们还没有在 contentEditable 中更新文本,否则插入符放置将引发索引超出范围的错误。因此,考虑到这一点,更新后的组件将如下所示: (ns your-app (:require [reagent.core :as r])) (defn contentEditable [] (let [ref (r/atom nil) content ( r/atom "") caret-location (r/atom nil)] (r/create-class {:component-did-update #(place-caret!ref content caret-location) :reagent-render (fn [] [ :div {:contentEditable true :ref #(fn [el] (reset!ref el)) :on-paste #(on-paste! content caret-location %) :on-input #(reset! content (.-innerHTML) (.-target %))) :dangerouslySetInnerHTML {:__html @content}}])}))) 是的!你可以看到我们也在传递到粘贴!函数一个名为 caret-location 的新状态变量,默认情况下将为 nil,我们将使用它来知道将插入符号与我们的位置插入符号放在何处!您可以看到正在从 :component-did-update 生命周期事件中调用函数。我们还创建了一个名为 ref 的新状态,它将保存 contentEditable 的实际 DOM 元素,以便我们知道我们将光标集中在哪个元素上。 (defn on-paste! [content caret-location event] (.then ( .readText (.-clipboard js/navigator)) (fn [clip] (let [pasted-content (parse-html clip) selection (.getSelection js/window) offset (.-anchorOffset selection) new-content (string-> string @content pasteed-content offset)] (reset! content new-content) (reset! caret-location (+ offset (count pasteed-content))))))) 所以现在 caret-location 将持有一个值无论粘贴时偏移量是多少 + 粘贴内容的长度,所以它现在应该在粘贴后立即出现。好吧,还没有 - 我们仍然需要创建我们的地方插入符!函数,所以让我们继续创建它看起来像这样: (defn place-caret! [ref content caret-location] (when (and (not (nil? @caret-location)) (>= (count @content) @caret -location) (first (.-childNodes @ref))) (let [selection (.getSelection js/window) range (.createRange js/document)] (.setStart range (first (.-childNodes @ref)) @caret -location) (.collapse range true) (.removeAllRanges selection) (.addRange selection range) (.focus @ref) (reset! caret-location nil))))
这个函数的作用是它需要一个 ref,它是 DOM 元素,例如我们的 contentEditable、内容和插入符号位置状态,然后它会确保内容不长于插入符号位置(因为如果是,我们将无法更改插入符号位置,因为索引超出范围)并且我们检查插入符号位置是否为 nil,因为它默认为 nil,因此我们只能在需要时调用插入符号位置,即我们的情况是在粘贴期间。毕竟是好的,我们获得当前选择,创建一个新范围,将范围的开始设置为我们的插入符位置,折叠该范围,从选择中删除所有现有范围并添加我们的新范围,然后我们'将专注于 ref 元素并重置插入符号位置状态。