使用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