H5可編輯屬性contenteditable實現(xiàn)富文本編輯器

使用HTML5新屬性contenteditable實現(xiàn)可插入鏈接贷痪、表情包城侧、其他變量的編輯器,因為在我使用這個功能是在19年項目中需求中有涉及,最近被問到一些關于該功能的問題,就做一下總結.

前序說明:

1、技術棧: vue@^2.7.14, element-ui@^2.13.1,?emoji@^0.3.2,??js, html, css
2咱筛、div可編輯屬性,change事件失效,可通過監(jiān)聽input事件來時時得到輸入內(nèi)容的變化
3搓幌、開發(fā)此功能是為了實現(xiàn)用微信公眾號向用戶推送客服消息時,創(chuàng)建文本消息內(nèi)容開發(fā)的,微信開放平臺對于文本消息("msgtype":"text",)內(nèi)容的格式有限制:文本中只支持a標簽
去微信開放平臺

微信開放平臺提供的接口:

先看頁面效果

可插入emoji表情,可插入a鏈接,可插入小程序鏈接,還可以插入一些自定義的變量

公眾號發(fā)送到用戶看到的效果

前期準備------首先簡單介紹一下contenteditable
contenteditable 屬性是 HTML5 中的新屬性。規(guī)定是否可編輯元素的內(nèi)容迅箩。

(1)屬性值
true 規(guī)定可以編輯元素內(nèi)容溉愁。
false 規(guī)定無法編輯元素內(nèi)容。

<div contentEditerable = true>此處內(nèi)容可編輯</div>

(2)contenteditable 與textarea的區(qū)別
? ?1.?textarea支持多行文本輸入饲趋,滿足了我們編輯的很大需求拐揭。然而,textarea不能像div一樣高度自適應奕塑,高度保持不變堂污,內(nèi)容大于高度時就會出現(xiàn)滾動條;
? ?2. textarea只支持文本輸入龄砰,隨著現(xiàn)在越來越關注用戶體驗盟猖,需求也越來越多讨衣,很多時候我們需要在編輯區(qū)域插入圖片,鏈接式镐,視頻值依;
? ?3.?傳統(tǒng)textarea文本域不能解析標簽,例如:extarea.value+=""http://輸入框內(nèi)仍然是愿险,不能解析標簽,自然就不能使用textarea作為文本載體了价说,我們可是使用conteneditable屬性,它可以讓你的div也具備輸入功能扮叨,div可以輸入內(nèi)容了,并且插入的標簽也可以解析狸捅;如果只是想在段落末尾加上表情衷蜓,那你大可以這樣去做:div. innerHTML+=""

(3)getSelection(獲取selection對象)
Selection對象所對應的是用戶所選擇的ranges(區(qū)域),俗稱拖藍尘喝。默認情況下磁浇,該函數(shù)只針對一個區(qū)域

var? sel =window.getSelection();
var? range = sel.getRangeAt(0)? //選擇第一選區(qū)
range.collapse(false);//對于IE來說,參數(shù)不可省略
range.insertNode(node);//節(jié)點插入到該選區(qū)

這樣去寫的話會存在一些問題朽褪;當你框選內(nèi)容的時候置吓,不會替換內(nèi)容,而是在所選內(nèi)容之后插入缔赠,這是因為range.collapse()方法

range.collapse();//(false默認)到選區(qū)末端衍锚,?true開始位置,?//當你框選內(nèi)容的時候嗤堰,執(zhí)行該方法戴质,可以讓光標移動到選區(qū)結束位置,然后插入內(nèi)容

所以梁棠,當框選的時候置森,正常做法應該是刪除框選內(nèi)容,然后插入新節(jié)點
range.deleteContents();//清除內(nèi)容

完整代碼:
functioninsertImg(src){
? ? ? if(window.getSelection) {
? ? ? ? ? ? var sel =window.getSelection();
? ? ? ? ? ? var range = sel.getRangeAt(0);
? ? ? ? ? ? var img =newImage();?
? ? ? ? ? ? range.deleteContents()
? ? ? ? ? ? img.src=src;
? ? ? ? ? ? range.insertNode(img);
? ? ? ? ? ? range.collapse(false);//對于IE來說符糊,參數(shù)不可省略
? ? ? ?}
?}

(4)contenteditable兼容性

(5)contenteditable其他知識點
讓contenteditable元素只能輸入純文本

css控制法
一個div元素凫海,要讓其可編輯,contenteditable屬性是最常用方法男娄,CSS中也有屬性可以讓普通元素可讀寫行贪。
user-modify (屬性介紹https://blog.csdn.net/weixin_30362233/article/details/98374335)
支持屬性值如下:

user-modify:read-only;
user-modify:read-write;
user-modify: write-only;//可以輸入富文本
user-modify:read-write-plaintext-only;//只能輸入純文本

read-write和read-write-plaintext-only會讓元素表現(xiàn)得像個文本域一樣漾稀,可以focus以及輸入內(nèi)容

(2)contenteditable控制法

contenteditable="plaintext-only"? ? //? "plaintext-only"可以讓編輯區(qū)域只能鍵入純文本

*注意:目前僅僅是Chrome瀏覽器支持比較好的


一、編輯器實現(xiàn)

1建瘫、輸入功能
div標簽可編輯
這一步比較簡單,只需要給div標簽添加contenteditable為true即可;

<div contenteditable="true" style="height:100px; border: 1px solid red; padding:2px;" id="editor" ref="editor">

</div>

通過監(jiān)聽input事件,時時關注內(nèi)容的變化并獲取輸入內(nèi)容

//let editor = document.getElementById('editor')
?//editor.addEventListener('input', (item) => { console.log(item) })

this.$refs.editor.addEventListener('input', this.changeContentValue);

自動獲取焦點

// let editor = document.getElementById('editor')
?// editor.focus();

this.$refs.editor.focus();

光標位置定位崭捍,往光標處插入html片段

// 往光標位置插入HTML片段
function insertHtmlAtCaret(html) {
? ? ?if (window.getSelection) {
? ? ? ? ?// IE9 and non-IE
? ? ? ? ? if (this.sel.getRangeAt && this.sel.rangeCount) {
? ? ? ? ? ? ? ? ? ?var el = document.createElement('div');
? ? ? ? ? ? ? ? ? ? el.innerHTML = html;
? ? ? ? ? ? ? ? ? ? var frag = document.createDocumentFragment();
? ? ? ? ? ? ? ? ? ? var node;
? ? ? ? ? ? ? ? ? ? var lastNode;
? ? ? ? ? ? ? ? ? ? while ((node = el.firstChild)) {
? ? ? ? ? ? ? ? ? ? ? ? ?lastNode = frag.appendChild(node);
? ? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ? ?this.range.insertNode(frag);
? ? ? ? ? ? ? ? ? if (lastNode) {
? ? ? ? ? ? ? ? ? ? ? ? ?this.range = this.range.cloneRange();
? ? ? ? ? ? ? ? ? ? ? ? ? this.range.setStartAfter(lastNode);
? ? ? ? ? ? ? ? ? ? ? ? ? this.range.collapse(true);
? ? ? ? ? ? ? ? ? ? ? ? ? this.sel.removeAllRanges();
? ? ? ? ? ? ? ? ? ? ? ? ? ?this.sel.addRange(this.range);
? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? }
? ? ? ? ? ? else if (document.selection && document.selection.type !== 'Control') {
? ? ? ? ? ? ? ? ? ? ?// IE < 9 document.selection.createRange().pasteHTML(html);
? ? ? ? ? ? ? }
? ? ?},

2、插入a鏈接功能
點擊插入鏈接按鈕可出現(xiàn)彈窗插入或者修改內(nèi)容

在點擊插入鏈接按鈕(也就是輸入框失去焦點)的時候獲取光標所在的位置
? ?this.sel = window.getSelection();
? ?this.range = this.sel.getRangeAt(0);
? ?this.taget = this.sel.focusNode.parentElement;
? ?const { sel, taget } = this;

選中一部分內(nèi)容,或者點解已插入鏈接的內(nèi)容
第一次添加鏈接或者多次修改鏈接內(nèi)容

? ?this.selectContents = sel.toString(); // 當選中未添加鏈接的內(nèi)容時啰脚,選中內(nèi)容復制給鏈接的文字字段

顯示彈窗,對彈窗的文本與鏈接進行修改
const { selectContents, selectUrl} = this;
this.$set(this.textForm, 'text', selectContents);
this.$set(this.textForm, 'url', selectUrl);

完成后點擊確定,以新內(nèi)容替換舊內(nèi)容
const { text, url } = this.textForm;
?if (text && url) {
?this.range && this.range.deleteContents(); // 刪除輸入框原有的文本內(nèi)容
?const { selectContents, selectUrl, taget } = this;
?if (selectContents && selectUrl && taget) {
?????Array.from(this.$refs.editor.childNodes).forEach((item) => {
?????????if (item === taget) {
? ? ? ? ? ? ? ? this.$refs.editor.removeChild(taget); // 刪除輸入框原有的文本鏈接內(nèi)容 }
? ? ? ? ? else if (taget.parentNode === item) {
?????????????????item.removeChild(taget); // 當村子a鏈接內(nèi)有插入了一次a標簽的情況處理
? ? ? ? ? ? ? ?}
? ? ? ? ? });
?????}

插入到輸入框
?this.insertHtmlAtCaret(`<a href='${url}' style="color:#5392ff">${text}</a>`); }?
?this.textForm = { url: '', text: '' }; // 重置

效果圖

3殷蛇、插入小程序鏈接同上

4、插入表情包功能
封裝emoji?組件

?
引入emoji組件

import Emoji from './emoji';
const emoji = require('emoji');
components: {
?Emoji
?}
html部分
<el-popover
? ???ref="popover-click"
? ???placement="bottom-start"
? ????width="390"
? ????trigger="click"
? ??????@show="mountedEmoji = true"
? >
? ? ? ? ?<Emoji
? ??????????@emoji = "selectEmoji">
? ??????</Emoji>
?</el-popover>

?插入表情
?function selectEmoji(emoji) {
? ? ?this.insertHtmlAtCaret(emoji);
?},

5橄浓、輸入字數(shù)統(tǒng)計功能
? ? ?div的可編輯屬性,獲取到的內(nèi)容格式如下,如果統(tǒng)計輸入字數(shù)需要對其進行處理

從獲取到的輸入內(nèi)容可得出的結論是
? (1) shift+回車換行會在當前操作的這一行后生成<br/>標簽,用來與下一行內(nèi)容分開
? (2) 直接回車換行會生成<div><br/></div>形式, 輸入內(nèi)容后,輸入的內(nèi)容替換div標簽中的br
? (3)當使用了直接回車換行,再使用shift+回車換行,則shift+回車換行這行內(nèi)容會被直接回車換行生成的div包裹
? (4) 光標處于0位置的時候禁止換行

針對以上需求處理方法是,對div中輸入的內(nèi)容進行過濾

function getDomValue(elem) {
? ??var res = '';
? ? let arr = Array.from(elem.childNodes);
? ???arr.forEach((child) => {
? ? ? ? ?if (child.nodeName === '#text') {
? ? ? ? ? ? ? ?res += child.nodeValue;
? ? ? ? ? } else if (child.nodeName === 'BR') {
? ? ? ? ? ? ? ? res += '\n';
? ? ? ? ? ?} else if (child.nodeName === 'P') {
? ? ? ? ? ? ? res += '\n' + getDomValue(child);
? ? ? ? ? ?} else if (child.nodeName === 'SPAN') {
? ? ? ? ? ?res += getDomValue(child);
? ? ? ? ? ?} else if (child.nodeName === 'BUTTON') {
? ? ? ? ? res += getDomValue(child);
? ? ? ? ? } else if (child.nodeName === 'IMG') {
? ? ? ? ? ? ?res += child.alt;
? ? ? ? ? ?} else if (child.nodeName === 'DIV') {
? ?????????????? const s = Array.from(child.childNodes);
? ? ? ? ? ? ??if (s.length === 1 && s[0].nodeName === 'BR' || child.previousSibling?&&?child.previousSibling.nodeName?===?'BR') {
?// 處理shift+回車與直接回車混用導致多處來換行的情況
? ? res += getDomValue(child); }
? ? ? ? ? ?else {
? ??????????????res += '\n' + getDomValue(child);?
? ? ? ? ? ? }
? ? ? ? ? ?)else if (child.nodeName === 'A') {
? ??????????????if (child.href !== null) {
?????????????????????const innerHTML = child.innerHTML.replace(/<br>/g, '')
????????????????????????????????????????.replace(/<span (.*?)>/gi, '').replace(/<\/span>/gi, '');
?????????????????????res += `<a href='${child.href}'>${innerHTML}</a>`;
?????????????????}
????????}
}

統(tǒng)計字數(shù)
function getDomValuelength(elem) {
?????var reg = /<a[^>]+?href=["']?([^"']+)["']?[^>]*>([^<]+)<\/a>/gi;
? ? ?var data = elem.toLowerCase().replace(reg, function ($1, $2, $3) {
? ? ? ? ? ? ?????????????return $3;
? ? ? ?????????});
? ? ? return data.length;
}

6粒梦、我的源碼git地址:https://github.com/wangAlisa/div-follow-input

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市荸实,隨后出現(xiàn)的幾起案子匀们,更是在濱河造成了極大的恐慌,老刑警劉巖准给,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泄朴,死亡現(xiàn)場離奇詭異,居然都是意外死亡露氮,警方通過查閱死者的電腦和手機祖灰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沦辙,“玉大人夫植,你說我怎么就攤上這事∮脱叮” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵延欠,是天一觀的道長陌兑。 經(jīng)常有香客問我,道長由捎,這世上最難降的妖魔是什么兔综? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮狞玛,結果婚禮上软驰,老公的妹妹穿的比我還像新娘。我一直安慰自己心肪,他們只是感情好锭亏,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著硬鞍,像睡著了一般慧瘤。 火紅的嫁衣襯著肌膚如雪戴已。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天锅减,我揣著相機與錄音糖儡,去河邊找鬼。 笑死怔匣,一個胖子當著我的面吹牛握联,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播每瞒,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼拴疤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了独泞?” 一聲冷哼從身側響起呐矾,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎懦砂,沒想到半個月后蜒犯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡荞膘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年罚随,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片羽资。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡淘菩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屠升,到底是詐尸還是另有隱情潮改,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布腹暖,位于F島的核電站汇在,受9級特大地震影響,放射性物質發(fā)生泄漏脏答。R本人自食惡果不足惜糕殉,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望殖告。 院中可真熱鬧阿蝶,春花似錦、人聲如沸黄绩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宝与。三九已至焚廊,卻和暖如春冶匹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背咆瘟。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工嚼隘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人袒餐。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓飞蛹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親灸眼。 傳聞我的和親對象是個殘疾皇子卧檐,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容