天坑之路:用js給選中文字添加樣式

来源:https://www.cnblogs.com/vvjiang/archive/2018/12/27/10186327.html
-Advertisement-
Play Games

前言 本例基於react,但是實際上就是用原生js做的。相容性做到了IE9,但是按照這個思路做是可以做到IE8甚至更低的。 需求與最初的思路 當我拿到這個需求的時候以為很簡單,就是可以給頁面上的文章做記號,比如添加個下劃線,或者背景塗色做成熒光筆的樣子。 因為只需要相容IE9,所以window.ge ...


前言

本例基於react,但是實際上就是用原生js做的。相容性做到了IE9,但是按照這個思路做是可以做到IE8甚至更低的。

需求與最初的思路

當我拿到這個需求的時候以為很簡單,就是可以給頁面上的文章做記號,比如添加個下劃線,或者背景塗色做成熒光筆的樣子。

因為只需要相容IE9,所以window.getSelection是支持的。(IE8及以下有其它的獲取選中的方法)

那麼思路就是選中文本,點擊添加下劃線後,通過 window.getSelection.getRangeAt(0) 拿到選中的文本對象,獲取到文本後,通過文本對象的 surroundContents 方法來將文本替換為帶有class的元素。

初步的實現

思路很簡單,代碼同樣也很簡單。

CSS代碼:

.custom-underline{
  border-bottom: 1px solid #f00;
  font-style: normal;
}

.nite-writer-pen{
  background-color: lightgreen;
  border-radius: 5px;
  box-shadow: 0 0 10px lightgreen;
  font-style: normal;
}

JS代碼:

/**
  * 用元素替換被選中的文本
  */
var replaceSelectedStrByEle = function(className){
  var selecter = window.getSelection();
  var selectStr = selecter.toString();
  if (selectStr.trim != "") {
    var rang = selecter.getRangeAt(0);
    var ele = document.createElement("i");
    ele.className = className;
    ele.textContent = selectStr
    rang.surroundContents(ele);
  }
}

replaceSelectedStrByEle('nite-writer-pen');

天坑出現

上面的思路實在是過於簡單,如果是一個很簡單的元素,那麼這種做法是沒有問題的。

但是我們的文章的html結構一般都沒有這麼簡單,比如對於以下情況:

<p>
  <p>道可道,非常道。</p>
  <p>名可名,非常名</p>
</p>

如果在頁面上我選中的操作如下:

那麼上面的代碼實現就會出現BUG,對於這種跨元素選中的情況,想當然的用元素去替換文本是沒用的。

如果你想得更多,比如跨多個元素選中,以及選中元素為更為複雜的html結構,你就會發現這是一個多大的坑。

html結構有多複雜,這個坑就有多深。

思路的僵局與寫輪眼

其實天坑也不是完全沒有路走,在跨多個元素選中的過程中,我想給選中的內容加樣式,那麼就需要獲取到所有選中的文本節點,並且批量替換成元素。

但是 window.getSelection.getRangeAt(0) 獲取到的range對象只能獲取到最開始選中的節點和最後選中的節點的。

那麼接下來通過選中的最開始的節點和最後的節點獲取到所有的文本節點。

思路就是這麼個思路,但是實現起來是很複雜的。

面臨深坑,肯定不可能硬剛。

畢竟我已經不是當年頭鐵的愣頭青了,做項目是有時間和精力成本的,如果要填掉這個坑,那麼加班是不可避免的,最重要的是在有限時間內填的這個坑可能還有各種BUG和相容性問題。

對待這種天坑,一般就給需求來個做不了三連了。

但是這把我想贏,畢竟這個東西看起來確實簡單。

在外人的眼裡,如此之簡單,分分鐘搞定的事情。這都做不了,我還怎麼在前端的圈子裡繼續划水?

所以我要動用程式員的入門技——寫輪眼。

然而百度、谷歌無效,根本沒有這個解決方案,有的全是些我最初的簡單實現。

但是回憶我們以前見到的各種網頁應用與場景,很容易就能想到上面的這種操作我們是見過的。

那就是從遠古IE時代就已經出現的各種富文本編輯器組件。

目標確認,百度的ueditor,這波我要贏。

分析ueditor與複製

上github兩三下拿到ueditor源碼,開始讀源碼分析代碼。

中間過程不再多說,精簡代碼,除去一些不需要的代碼和相容性處理後,拿到了五個文件:

  • browser.js (瀏覽器版本判斷,用於做相容性處理)
  • domUtils.js (dom操作)
  • dtd.js (節點的類型與元素判斷)
  • Range.js (封裝的選中範圍對象)
  • utils.js (工具類)

即使精簡後,代碼也不少,大概兩三千行。不過其中還有很多註釋,壓縮後體積並不大。

由於代碼比較多,這裡就不全部展示了,文章最後會給出github的地址。

這裡只給出最後的使用代碼:

/**
 * 添加下劃線
 */
addUnderline = () => {
  this.replaceSelectedStrByEle(styles['custom-underline'])
}

/**
 * 啟用熒光筆
 */
enableNiteWriterPen = () => {
  this.replaceSelectedStrByEle(styles['nite-writer-pen'])
}

/**
 * 用元素替換被選中的文本
 */
replaceSelectedStrByEle = (className) => {
  var getRange = () => {
    var me = window;
    var range = new Range(me.document);

    var sel = window.getSelection();
    if (sel && sel.rangeCount) {
      var firstRange = sel.getRangeAt(0);
      var lastRange = sel.getRangeAt(sel.rangeCount - 1);
      range.setStart(firstRange.startContainer, firstRange.startOffset)
        .setEnd(lastRange.endContainer, lastRange.endOffset);
    }
    return range
  }
  var range = getRange();
  range.applyInlineStyle('i', {
    class: className
  });
  range.select();
}

使用起來還是比較簡單的。

對i元素的處理做的一些修改

如果我們選中的是已經被包裹在i元素中的一段文本,那麼調用後會發現並沒有加上class屬性。

這是因為富文本編輯器和我們的需求不一樣,通過操作想把選中文本變為i元素,而文本外面本來就是i元素了,自然不會進行剩下的操作。

在填充元素的最後會有一個mergeToParent的操作,他會在填充元素的標簽和其父級元素的標簽一樣後將元素替換為文本。

if (parent.tagName == node.tagName || parent.tagName == "A") {
  //...
}

那麼這裡我們要修改源碼加上一個判斷

if ((parent.tagName == node.tagName && parent.className == node.className) || parent.tagName == "A") {
  //...
}

至於其它的邏輯保持不變即可。

為支持回退操作做的一些修改

這裡getRange拿到的對象range在選中內容並替換樣式後依然可以使用。

可以調用

range.removeInlineStyle('i')

移除之前添加的樣式。

也就是說這裡如果使用一個命令模式之類的,是可以實現回退操作的。

不過這裡還是有一個坑,就是removeInlineStyle會移除掉選中內容中所有的i元素,於是我修改了

Range.js中removeInlineStyle這個方法,多加了一個className參數,每次去掉i元素時都會判斷是否參數等於className。

然後我們調用時就是

range.removeInlineStyle('i',styles['nite-writer-pen'])

總結

作為一個暗藏天坑的小需求,搞定之後其實還挺有成就感的。

粗略閱讀了源碼後才發現如果自己做會有多坑,基本上沒個三五天下不來,並且在多掉了N根頭髮後仍然會發現到處都是考慮不周和各種BUG。

那麼最後貼上代碼的github地址:github地址

如文中有謬誤,或者您有更有趣的玩法,還望不吝賜教。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 寫前端代碼,尤其是做一個前端框架的時候,經常需要及時知道代碼的大致性能,這時候如果能有個好的辦法能一直看到當前頁面的fps就好了。 整體思路是一秒有一千毫秒,先記錄當前時間作為最後一次記錄fps的時間,通過 requestAnimationFrame 回調不斷給累加fsp計數器,並且判斷上次記錄fp ...
  • 圓角,其css如下 這沒什麼難的,記憶一下即可。但是深入一下,還是很有講究的。 問題1,div寬高皆為100px,border-radius:30px,這個30px是怎麼工作的?換成50px,70px,甚至200px會有什麼反應? 答:等於一個100px的正方形,然後用半徑為30px的圓來過渡邊角, ...
  • Vue 項目在開發時運行正常,打包發佈後卻出現各種報錯,這裡整理一下遇到的問題,以備忘。 1、js 路徑問題 腳手架預設打包的路徑為絕對路徑,改為相對路徑。修改 config/index.js 中 build 節點下 assetsPublicPath,把原來 ‘/’ 改為 ‘./’ 2、img 路徑 ...
  • jQuery是什麼? jQuery是一款優秀的JavaScript庫,從命名可以看出jQuery最主要的用途就是用來做查詢(jQuery=js+Query),正如jQuery官方Logo副標題所說(write less,do more)使用jQuery能讓我們對HTML文檔遍歷和操作、事件處理、動畫 ...
  • Node.js 10已經進入LTS時代!其應用場景已經從腳手架、輔助前端開發(如SSR、PWA等)擴展到API中間層、代理層及專業的後端開發。Node.js在企業Web開發領域也日漸成熟,無論是在API中間層,還是在微服務中都得到了非常好的落地。本書將通過Web開發框架Koa2,引領你進入Node.... ...
  • 測試Mantis rest api時碰到的問題:404 Not Found. 根據文件,Mantis rest api的預設url是{{url}}/api/rest/{{controller}}。 其中{{url}}的部分是Mantis實體的base url,{{controller}}則是各api ...
  • 1、HTML代碼,如下圖: 2、jQuery代碼,如下圖: ...
  • 本文由雲+社區發表 目的 寫了幾個Flutter的demo,但是對Flutter的自定義view和動畫都不太瞭解,看到一個類似效果在android的實現,就嘗試用Flutter做一下。同時也是學習Flutter的自定義view和動畫相關的知識。 效果 效果動圖 在藍色區域點擊,會產品水波紋動畫。 宛 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...