前置知識
好了,交代完了背景,讓我們先補充一些基礎知識吧,不懂的請務必不要跳過!
contenteditable 屬性
假如我們給一個標簽加上 contenteditable="true" 的屬性,就像這樣:
<div contenteditable="true"></div>
那么在這個 div 中我們就可以對其進行任意編輯了。如果想要插入的子節點不可編輯,我們只需要把子節點的屬性設置為 contenteditable="false" 即可,就像這樣:
<div contenteditable="true"> <p>這是可編輯的</p> <p contenteditable="false">這是不可編輯的</p></div>
該屬性最早是在 IE 上實現的(厲害哦),且可以作用於其它標簽,不限於 div,大家應該或多或少都聽說過這個屬性。
document.execCommand 方法
https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand
既然我們可以對上面的 div 隨意編輯,那具體怎么編輯呢,目前好像也還是只能輸入文本,要怎樣才能進行其他操作呢(比如加粗、傾斜、插入圖片等等)?其實瀏覽器給我們提供了這樣的一個方法 document.execCommand,通過它我們就能夠操縱上面的可編輯區。具體語法如下:
其中第一個參數就是一些命令名稱,具體的可以查看 MDN;第二個參數寫死為 false 就行了,因為早前 IE 有這樣一個參數,為了兼容吧,不過這個參數在現代瀏覽器中是沒有影響的;第三個參數就是一些命令可能需要額外的參數,比如插入圖片就要多傳個 url 或 base64 的參數,沒有的話傳個 null 就行。
我們簡要列舉下它的幾個使用方式,大家就知道怎么用了:
這個命令就是富文本的核心(所以務必記住),瀏覽器把大部分我們能想到的功能也都實現了,當然各瀏覽器之間還是有差異的,這里就不考慮了。
Selection 和 Range 對象
我們在執行 document.execCommand 這個命令之前首先要知道對誰執行,所以這里會有一個選區的概念,也就是 Selection 對象,它用來表示用戶選擇的范圍或光標位置(光標可以看做是范圍重合的特殊狀態),一個頁面用戶可能選擇多個范圍(比如 Firefox)。也就是說 Selection 包含一個或多個 Range 對象(Selection 可以說是 Range 的集合),當然對於富文本編輯器來說,一般情況下,我們只會有一個選擇區域,也就是一個 Range 對象,事實上大部分情況也是如此。
所以通常我們可以用 let range = window.getSelection().getRangeAt(0) 來獲取選中的內容信息(getRangeAt 接受一個索引值,因為會有多個 Range,而現在只有一個,所以寫0)。
看得一頭霧水?沒關系,看下面兩張圖就懂了:
一句話說就是:通過上面那句命令我們能夠獲取到當前的選中信息,一般會先保存下來,然后在需要的時候還原。此外 Selection 對象還有幾個常用的方法,addRange、removeAllRanges、collapse 和 collapseToEnd 等等。
這個知識點是很重要的,因為它讓我們有了操縱光標的能力(比如插入內容之后設置光標的位置),不過這篇文章中我並沒有去深入它,只是淺出。
目標
開篇一頓扯,下面讓我們抓緊時間做一個屬於自己的富文本吧,大概會包含以下幾個功能:加粗、段落、H1、水平線、無序列表、插入鏈接、插入圖片、后退一步、向前一步等等。,Let's do it!
起步
首先一個富文本大體分為兩個區域,一個是按鈕區,一個是編輯區。所以它的大致結構就像下面這樣:
嗯,起步工作到此結束,接下來就可以直接開始實現功能了。
加粗
現在假如我們要實現加粗的效果,該怎么做呢?很簡單,只要在點擊加粗按鈕的時候執行 document.execCommand('bold', false, null) 這句話,就能達到加粗的效果,就像下面這樣:
讓我們運行一下看看效果:
嗯,是的,就是這么簡單的一句話就能搞定。
當然了,我們開篇也說了我們的一切命令都是基於 document.execCommand 的,所以我們先小小改寫一下上面代碼中的 execCommand 方法,就像下面這樣:
這樣一來代碼就更具通用性了。實現列表、水平線、前進、后退功能和加粗是一樣樣的,只需傳入不同的命令名即可,就像下面這樣,這里就不一一贅述了:
順帶給大家說幾個注意點✍️:
1. 有的同學可能用的不是 button 標簽,然后執行命令就會無效,是因為點擊其他標簽大多都會造成先失去焦點(或者不知不覺就突然失去焦點了),再執行點擊事件,此時沒有選區或光標所以會沒有效果,這點要留意一下。
2. 我們執行的是原生的 document.execCommand 方法,瀏覽器自身會對 contenteditable 這個可編輯區維護一個 undo 棧和一個 redo 棧,所以我們才能執行前進和后退的操作,如果我們改寫了原生方法,就會破壞原有的棧結構,這時就需要自己去維護,那就麻煩了。
3. style 里面如果加上 scope 的話,里面的樣式對編輯區的內容是不生效的,因為編輯區里面是后來才創建的元素,所以要么刪了 scope,要么用 /deep/ 解決(Vue 是這樣)。
段落
這個功能就是把光標所在行的文字用 p 標簽包裹起來,為了演示方便,我們順便把編輯區的 html 結構打印出來,所以讓我們稍微改一下代碼,就像下面這樣:
運行效果如下:
怎么樣,是不是也很 easy,同理,h1 ~ h6 也是一樣的,命令為 execCommand('formatBlock', '<h1>'),也不贅述了。
插入鏈接
這個功能因為需要第三個參數,所以我們一般會給個提示框獲取用戶輸入,然后再執行 execCommand('createLink', 鏈接地址),代碼如下:
效果如下:
插入圖片鏈接也是異曲同工,只不過命令名不一樣而已:
插入圖片
圖片除了可以通過添加地址的形式外,還可以添加 base64 格式的圖片,這里我們通過 readAsDataURL(file) 來讀取圖片,並執行 execCommand('insertImage', base64) 就大功告成啦,具體代碼如下,並不復雜:
<button class="nav__img">插入圖片 <!--這個 input 是隱藏的--> <input type="file" accept="image/gif, image/jpeg, image/png" @change="insertImg"></button>
insertImg(e) { let reader = new FileReader(); let file = e.target.files[0]; reader.onload = () => { let base64Img = reader.result; this.execCommand('insertImage', base64Img); document.querySelector('.nav__img input').value = ''; // 解決同一張圖片上傳無效的問題 }; reader.readAsDataURL(file);}
運行一下,看看效果:
這應該也不是很難。當然了,你也可以先上傳到服務器處理返回 url 地址再插入也是可以的。
至此,一個簡易版的富文本就完成了(當然了 bug 也是有的,不過並不妨礙我們理解),具體代碼可以參考 npm 上的 pell 包,它已經是個極簡版的了。
進階
其實富文本對文本的操作大多都可以用原生命令來實現,但是對圖片的操作也許就不那么容易了,來個拉伸、旋轉啥的就夠我們折騰了,所以這里以圖片拉伸為例子着重講解一下。
圖片拉伸
我們先看下大致效果,大家也可以先停下來思考一分鍾看看如何實現:
,首先我們要知道的是圖片已經在編輯區了,所以當用戶點擊編輯區里面的圖片時我們需要做些事件監聽並有所處理,具體思路如下(這部分代碼較多,不想看的可以略過,但標題要看):
1. 判斷用戶點擊的是否是編輯區里面的圖片
這個就是看點擊事件 e.target.tagName 是不是 img 標簽了,代碼如下,應該比較簡單:
2. 在點擊的圖片上創建個大小一樣的 div
如果點擊的是一個圖片,那我們就創建一個 div ,暫且把這個 div 叫做蒙層吧,順便先看張示意圖:
也就是動態創建一個蒙層(和圖片一樣大小)以及四個拖拽頂點,並定位到和圖片一樣的位置,代碼如下(代碼有點多,可跳過,知道大致意思就行):
3. 在四個頂點框上添加拖拽事件
這里我們會在四個頂點監聽 mousedown 事件,按下鼠標時,首先會改變鼠標樣式(就是鼠標會變成調整大小的那種圖標),然后監聽 mousemove 和 mouseup 事件,計算出水平拖拽距離,然后重新設置圖片大小和浮層大小,大概這么個意思,簡要代碼如下:
當然問題還是有的,不過我們知道這個思路就行。具體代碼可以去看下 npm 上的 quill-image-resize-module 包,我也是按照這個包的思路來講解的。。。
操縱光標
除了不好對圖片進行處理外,光標應該也是一大坑,你可能不知道什么時候就失去焦點了,此時再點擊按鈕執行命令就無效了;有時你又需要還原或設置光標的位置,比如插入圖片后,光標要設置到圖片后面等等之類的。
所以我們需要具有控制光標的能力,具體操作就是在點擊按鈕之前我們可以先存儲當前光標的狀態,執行完命令或者在需要的時候后再還原或設置光標的狀態即可。由於在 chrome 中,失去焦點並不會清除 Seleciton 對象和 Range 對象,所以就像我一開始說的我沒怎么去了解。。。這里就只簡要展示兩個方法給大家看下:
以上就是今天所要分享的內容,感謝你的閱讀,大贊無疆 。。。。
結語
回到開頭我們講的那個需求,關於圖片旋轉的,根據上面的思路,你可以在蒙層上加個旋轉圖標,並添加點擊事件,然后修改圖片和蒙層 transform 屬性,當然了位置也要變,可能需要些計算,我也沒試過,不知道效果咋樣。
另外一種方法就是在插入圖片之前先對圖片進行處理(比如多一步類似裁剪的功能)再上傳,這樣就可以不用在編輯區里面處理圖片啦,嘿嘿,目前我就想到這兩種方案了,實際工作中采用的是第二種方
作者:尤水就下
來源:掘金
原文鏈接:https://juejin.im/post/5cfe4e8a6fb9a07ec63b09a4#heading-0
式,因為產品的需求不止於旋轉。
