
TinyMCE Sugar 9.3.0 版本演進全解DOM 封裝庫的核心 API 變更與源碼深度解析【免費下載鏈接】tinymceThe worlds #1 JavaScript library for rich text editing. Available for React, Vue and Angular項目地址: https://gitcode.com/gh_mirrors/ti/tinymce導讀Sugar 是 TinyMCE 開源倉庫中的核心 DOM 操作庫它為原生瀏覽器 DOM API 提供了類型安全、函數(shù)式的封裝。本文基于倉庫內(nèi) modules/sugar/CHANGELOG.md 與 .changes/sugar/9.3.0.md 的版本記錄系統(tǒng)梳理 Sugar 從 8.0.0 到 9.3.0 的關(guān)鍵 API 演進包括Focus.focus的防滾動聚焦、Awareness.isCursorPosition對contenteditablefalse的支持、Ready.image異步資源加載、Remove.unwrap與Replication.mutate插入順序調(diào)整以及ContentEditable新模塊的引入。讀完本文你將理解這些變更背后的設(shè)計動機與源碼實現(xiàn)并掌握在 TinyMCE 生態(tài)中正確使用這些 API 的實戰(zhàn)方法。一、Sugar 庫在 TinyMCE 生態(tài)中的定位Sugar位于 modules/sugar是 TinyMCE 底層工具鏈的一部分同 Katamari函數(shù)式工具集、Sand跨瀏覽器平臺檢測、AlloyUI 組件框架等模塊協(xié)同工作。它以SugarElementT這一輕量包裝結(jié)構(gòu)為核心——內(nèi)部僅持有原生 DOM 節(jié)點的引用通過函數(shù)式 API 完成對節(jié)點、屬性、樣式、事件、選區(qū)、尺寸等領(lǐng)域的操作。Sugar 的源碼目錄結(jié)構(gòu)清晰地劃分了職責領(lǐng)域見 modules/sugar/src/main/ts/ephox/sugar/apidom/DOM 操作如Focus、Remove、Replication、Insertevents/事件處理與就緒檢測如DomEvent、Readynode/節(jié)點類型判斷如SugarNode、SugarElementproperties/屬性與樣式如Attribute、Class、ContentEditablesearch/遍歷與查詢?nèi)鏣raverse、SelectorFindselection/選區(qū)與光標位置如Awareness、WindowSelectionview/視口與尺寸如Width、Height、WindowVisualViewport下面按照版本號從新到舊的順序逐一解析 9.3.0、9.2.0、9.1.0、9.0.0、8.1.0、8.0.0 六個版本的核心變更。二、9.3.0Focus.focus新增preventScroll參數(shù)2023-11-22變更內(nèi)容9.3.0 版本的唯一變更是對Focus.focus函數(shù)的改進TheFocus.focusfunction now takes an additionalpreventScrollparameter to allow focus on an element without scrolling.即Focus.focus現(xiàn)在接受一個額外的preventScroll參數(shù)允許在不滾動頁面的情況下將焦點賦予元素。源碼實現(xiàn)查看 Focus.tsconst focus (element: SugarElementHTMLElement, preventScroll: boolean false): void element.dom.focus({ preventScroll });實現(xiàn)非常簡潔Sugar 將preventScroll直接透傳給原生HTMLElement.focus()的 options 對象。默認值為false因此這是一個完全向后兼容的增強——現(xiàn)有調(diào)用Focus.focus(element)的代碼行為不變?nèi)詴|發(fā)滾動。實戰(zhàn)用法import * as Focus from ephox/sugar/api/dom/Focus; import { SugarElement } from ephox/sugar/api/node/SugarElement; const input SugarElement.fromTag(input); // 聚焦但不滾動頁面例如恢復編輯器光標時避免視口跳動 Focus.focus(input, true);同族 API 一覽Focus模塊還提供以下相關(guān)函數(shù)Focus.ts函數(shù)說明focus(element, preventScroll?)聚焦指定元素blur(element)使元素失焦hasFocus(element)判斷元素是否持有焦點通過root.activeElement比對active(root?)返回當前焦點元素OptionalSugarElementT支持傳入 ShadowRootsearch(element)查找元素內(nèi)部已聚焦的后代優(yōu)先于:focus選擇器不依賴鍵盤焦點狀態(tài)focusInside(element)若元素內(nèi)部尚無焦點則聚焦元素本身其中active與search均通過SugarShadowDom.getRootNode支持 Shadow DOM 場景。倉庫中的測試 FocusTest.ts 覆蓋了普通文檔與 ShadowRoot 兩種環(huán)境下active、search、hasFocus、focusInside的行為例如驗證 ShadowRoot 的 activeElement 是內(nèi)部輸入框、而 Document 的 activeElement 是 shadow host。三、9.2.0Awareness.isCursorPosition支持contenteditablefalse2023-03-15變更內(nèi)容Awareness.isCursorPositionAPI now returnstruefor passedcontenteditablefalseelements.即Awareness.isCursorPosition現(xiàn)在對contenteditablefalse的元素返回true。這意味著非可編輯元素也可以成為合法的光標??课恢眠@對 TinyMCE 中處理圖片、嵌入對象等不可編輯內(nèi)容的光標定位至關(guān)重要。源碼實現(xiàn)awareness.ts 中的判斷邏輯const isContentEditableFalse (elem: SugarElementNode) SugarNode.isHTMLElement(elem) (Attribute.get(elem, contenteditable) false); const elementsWithCursorPosition [ img, br ]; const isCursorPosition (elem: SugarElementNode): boolean { const hasCursorPosition isTextNodeWithCursorPosition(elem); return hasCursorPosition || Arr.contains(elementsWithCursorPosition, SugarNode.name(elem)) || isContentEditableFalse(elem); };isCursorPosition判斷一個節(jié)點是否可以作為光標位置共三種情況非空文本節(jié)點文本內(nèi)容去除空白后非空或包含nbsp;Unicode.nbsp見isTextNodeWithCursorPosition固有光標元素img與brcontenteditablefalse的 HTML 元素9.2.0 新增。Awareness模塊還配套提供getEnd、isEnd、isStart等函數(shù)用于計算元素的光標邊界文本節(jié)點取其字符長度img固定為 1其余取子節(jié)點數(shù)。四、9.1.0SugarNode.isHTMLElement增加nodeType前置校驗2022-09-08變更內(nèi)容TheSugarNode.isHTMLElementfunction now ensures thenodeTypeis1before checking the prototypes.即SugarNode.isHTMLElement在檢查原型鏈之前先確保nodeType 1元素節(jié)點。源碼實現(xiàn)SugarNode.tsconst isHTMLElement (element: SugarElementNode): element is SugarElementHTMLElement isElement(element) SandHTMLElement.isPrototypeOf(element.dom);其中isElement是isTypeElement(NodeTypes.ELEMENT)即校驗nodeType 1。這一前置檢查避免了對文本節(jié)點、注釋節(jié)點等非元素節(jié)點調(diào)用SandHTMLElement.isPrototypeOf時可能出現(xiàn)的誤判或性能開銷使類型守衛(wèi)更加嚴謹。isHTMLElement與isTag、isText、isDocument等共同構(gòu)成了 Sugar 基于nodeType的節(jié)點類型判別體系。五、9.0.0破壞性變更與跨瀏覽器支持收縮2022-03-039.0.0 是這一系列中變更最密集的版本包含新增、變更、移除、修復四類改動。5.1 新增Ready.image圖片加載完成后再繼續(xù)NewReady.imagefunction that returns a promise which will not resolve until the image element has loaded. Errors trigger promise rejection.Ready.ts 的實現(xiàn)const image (image: SugarElementHTMLImageElement): PromiseSugarElementHTMLImageElement new Promise((resolve, reject) { const loaded () { destroy(); resolve(image); }; const listeners [ DomEvent.bind(image, load, loaded), DomEvent.bind(image, error, () { destroy(); reject(Unable to load data from image: image.dom.src); }), ]; const destroy () Arr.each(listeners, (l) l.unbind()); if (image.dom.complete) { loaded(); } });實現(xiàn)要點通過DomEvent.bind同時監(jiān)聽load與error事件若圖片已經(jīng)緩存完成image.dom.complete true立即 resolve避免死等加載失敗時 reject 并攜帶圖片src信息方便排查無論成功失敗都會解綁監(jiān)聽器避免內(nèi)存泄漏。實戰(zhàn)示例import * as Ready from ephox/sugar/api/events/Ready; const img SugarElement.fromTag(img); img.dom.src https://example.com/hero.png; Ready.image(img).then( (loaded) console.log(圖片加載完成, loaded.dom.src), (err) console.error(加載失敗, err) );同文件還提供了Ready.document即 9.0.0 之前的Ready.execute與Ready.video。其中Ready.document根據(jù)document.readyState判斷若已是complete或interactive則立即執(zhí)行回調(diào)否則監(jiān)聽DOMContentLoaded后執(zhí)行一次并解綁Ready.ts。5.2Ready.execute更名為Ready.documentRenamedReady.executetoReady.document, for better clarity on what it does這是一次破壞性命名變更舊名稱Ready.execute語義模糊新名稱Ready.document明確表達了等待文檔就緒的意圖。遷移時需將Ready.execute(fn)改為Ready.document(fn)。模塊導出語句也印證了這一點Ready.tsexport { documentReady as document, image, video };5.3Remove.unwrap與Replication.mutate插入順序調(diào)整Remove.unwrapAPI now inserts children after the current node, instead of before.Replication.mutateAPI now inserts the replacement node after the current node, instead of before.這兩處調(diào)整將解包/替換操作中原有的前插before改為后插after。從 DOM 遍歷語義看新順序更符合直覺子節(jié)點或替換節(jié)點緊跟在原節(jié)點之后保持后續(xù)兄弟節(jié)點的相對位置穩(wěn)定。查看 Remove.tsconst unwrap (wrapper: SugarElementNode): void { const children Traverse.children(wrapper); if (children.length 0) { InsertAll.after(wrapper, children); } remove(wrapper); };以及 Replication.tsconst mutate K extends keyof HTMLElementFullTagNameMap (original: SugarElementElement, tag: K): SugarElementHTMLElementFullTagNameMap[K] { const nu shallowAs(original, tag); Insert.after(original, nu); const children Traverse.children(original); InsertAll.append(nu, children); Remove.remove(original); return nu; };unwrap的典型應(yīng)用是去掉包裹層例如將bspantext/span/b中的b去掉子節(jié)點span會被移到b之后即原位置再刪除b本身。mutate則用于原地換標簽先創(chuàng)建一個同屬性新標簽shallowAs會通過Attribute.clone復制全部屬性插入到原節(jié)點之后把原節(jié)點的所有子節(jié)點搬入新節(jié)點最后刪除原節(jié)點——例如在表格單元格td與th之間切換時即可復用該邏輯源碼注釋中也提到了這一使用場景。5.4 升級 Katamari 9.0 與移除舊瀏覽器支持Upgraded to Katamari 9.0, which includes breaking changes to theOptionalAPI used in this module. Removed support for Microsoft Internet Explorer and legacy Microsoft Edge.Katamari 是 Sugar 的基礎(chǔ)工具庫Optional、Arr、Obj等均來自ephox/katamari。9.0.0 同步升級到 Katamari 9.0其OptionalAPI 的破壞性變更如getOrDie、fold等簽名調(diào)整會傳導到 Sugar 的公開接口因此本次也屬于破壞性版本。同時Sugar 正式移除對 IE 與舊版 EdgeEdgeHTML 內(nèi)核的支持后續(xù)代碼可以依賴現(xiàn)代瀏覽器 API如classList、Promise、Shadow DOM而無需降級兼容——這一點在后續(xù) 11.0.0 的 CHANGELOG 中也有呼應(yīng)Fallback code which was only required on browsers that are no longer supported。5.5 修復Class.toggle的空 class 屬性殘留TheClass.toggleAPI didnt cleanup the class attribute when empty.修復前當最后一個 class 被 toggle 掉后元素上會殘留空的class屬性。修復方式是在 Class.ts 中引入cleanClassconst cleanClass (element: SugarElementElement): void { const classList ClassList.supports(element) ? element.dom.classList : ClassList.get(element); // classList is a live list, so this is up to date already if (classList.length 0) { // No more classes left, remove the class attribute as well Attribute.remove(element, class); } };remove與toggle在操作完成后都會調(diào)用cleanClass當classList長度為 0 時通過Attribute.remove徹底移除class屬性。toggler工廠函數(shù)的off回調(diào)也做了同樣處理。這樣生成的 DOM 更干凈也避免了一些對空 class 屬性敏感的 CSS 選擇器或序列化場景出現(xiàn)問題。六、8.1.0遍歷、批量屬性與尺寸 API 擴充2021-10-116.1 新增Traverse.parentElementparentElement返回元素的父元素OptionalSugarElementHTMLElement與parent/parentNode的區(qū)別在于它基于原生element.parentElement只會命中元素節(jié)點跳過文本節(jié)點等非元素父節(jié)點Traverse.tsconst parentElement (element: SugarElementNode): OptionalSugarElementHTMLElement Optional.from(element.dom.parentElement).map(SugarElement.fromDom);Traverse模塊還提供owner、parents、siblings、prevSibling、nextSibling、children、leaf等遍歷原語parents支持傳入isRoot謂詞提前終止向上遍歷適用于需要沿祖先鏈搜索但不超過某個邊界的場景。6.2 新增Attribute.setOptions與Css.setOptionsAttribute.setOptions接受一個值為Optional的批量屬性表值為some(v)時設(shè)置屬性為none()時移除該屬性Attribute.tsconst setOptions (element: SugarElementElement, attrs: Recordstring, Optionalstring | boolean | number): void { Obj.each(attrs, (v, k) { v.fold(() { remove(element, k); }, (value) { rawSet(element.dom, k, value); }); }); };實戰(zhàn)示例——根據(jù)條件設(shè)置disabled或移除import * as Attribute from ephox/sugar/api/properties/Attribute; import { Optional } from ephox/katamari; const disabled Optional.some(true); Attribute.setOptions(button, { disabled, title: Optional.none() }); // disabled 被設(shè)為 truetitle 被移除與之對應(yīng)的底層rawSet僅接受 string / boolean / number 三類值其余類型會console.error并拋錯避免把非法值寫入 DOM。Css.setOptions語義相同值為Optionalstring用于按條件設(shè)置或清除內(nèi)聯(lián)樣式Css.ts。6.3 新增Width.getInner/Height.getInner與getRuntime8.1.0 為尺寸 API 增加了四個函數(shù)Width.getInner/Height.getInner獲取元素的內(nèi)容區(qū)尺寸不含 padding/border對應(yīng)clientWidth/clientHeightWidth.getRuntime/Height.getRuntime獲取運行時實際渲染尺寸。實現(xiàn)位于 Width.ts 與 Height.ts內(nèi)部委托給 impl/RuntimeSize.ts 完成具體測量。這讓調(diào)用方可以按需選擇文檔聲明的 CSS 尺寸與瀏覽器實際布局后的尺寸在計算滾動容器、彈層定位等場景中非常實用。6.4 修復 Firefox 的window.visualViewport誤報Disabledwindow.visualViewportin Mozilla Firefox as it was returning an incorrect value forpageTopwhen usingposition: fixed.WindowVisualViewport.ts 中通過平臺檢測禁用了 Firefox 下的visualViewportconst get (_win?: Window): OptionalVisualViewport { const win _win undefined ? window : _win; if (PlatformDetection.detect().browser.isFirefox()) { // TINY-7984: Firefox 91 is returning incorrect values for visualViewport.pageTop, so disable it for now return Optional.none(); } else { return Optional.from(win.visualViewport); } };源碼注釋引用了內(nèi)部問題號 TINY-7984Firefox 91 在position: fixed場景下visualViewport.pageTop返回值不正確因此 Sugar 對 Firefox 回退到documentElement.clientWidth/clientHeight加滾動偏移的方式計算邊界getBounds。getBounds中還對 iOS 的pageLeft/pageTop與滾動位置取了最大值Math.max以規(guī)避scrollIntoView()不更新 pageTop 的兼容性問題。七、8.0.0新增ContentEditable模塊2021-08-26變更內(nèi)容Added newContentEditablemodule to determine if an HTML element is content editable.8.0.0 引入了獨立的ContentEditable模塊集中處理元素是否可編輯的判斷與設(shè)置此前這類邏輯散落在各處。源碼實現(xiàn)ContentEditable.ts 提供五個函數(shù)函數(shù)說明get(element)返回元素是否可編輯布爾值getRaw(element)返回原生element.dom.contentEditable字符串true/false/inheritset(element, editable)設(shè)置contentEditable為true或falseclosest(target)沿祖先鏈查找最近的[contenteditable]元素isEditable(element, assumeEditable?)綜合判斷可編輯性isEditable的實現(xiàn)值得注意ContentEditable.tsconst isEditable (element: SugarElementHTMLElement, assumeEditable: boolean false): boolean { if (SugarBody.inBody(element)) { return element.dom.isContentEditable; } else { // Find the closest contenteditable element and check if its editable return closest(element).fold( Fun.constant(assumeEditable), (editable) getRaw(editable) true ); } };元素已在文檔中時直接使用瀏覽器計算后的isContentEditable會綜合繼承狀態(tài)元素尚未掛載到文檔時isContentEditable不可靠改為向上查找最近的[contenteditable]祖先判斷其原始屬性是否為true若找不到任何祖先則回退到assumeEditable參數(shù)默認false。與 9.2.0 變更的呼應(yīng)8.0.0 的ContentEditable模塊與 9.2.0 的Awareness.isCursorPosition變更形成了完整的能力閉環(huán)ContentEditable負責判斷和設(shè)置可編輯性Awareness.isCursorPosition負責在光標定位時承認contenteditablefalse元素是合法的光標停靠點。兩者共同支撐 TinyMCE 在富文本中處理不可編輯內(nèi)容如圖片、嵌入對象時的選區(qū)與光標邏輯。升級注意8.0.0 同樣聲明升級了 Katamari 8.0OptionalAPI 存在破壞性變更這意味著使用 Sugar 8.0.0 時需同步升級依賴的 Katamari 版本。八、版本演進速查表與升級建議版本日期類型核心內(nèi)容9.3.02023-11-22ImprovedFocus.focus新增preventScroll參數(shù)9.2.02023-03-15ChangedAwareness.isCursorPosition對contenteditablefalse返回true9.1.02022-09-08ImprovedSugarNode.isHTMLElement先校驗nodeType 19.0.02022-03-03Breaking新增Ready.imageReady.execute更名Ready.documentRemove.unwrap/Replication.mutate改后插升級 Katamari 9.0移除 IE/舊 Edge 支持修復Class.toggle8.1.02021-10-11Added/Fixed新增Traverse.parentElement、Attribute.setOptions、Width/Height的getInner/getRuntime禁用 FirefoxvisualViewport8.0.02021-08-26Breaking新增ContentEditable模塊升級 Katamari 8.0針對不同場景的升級建議從 8.x 升 9.x重點關(guān)注Ready.execute→Ready.document的重命名以及Remove.unwrap、Replication.mutate插入順序變化對 DOM 結(jié)果的潛在影響若你的代碼依賴子節(jié)點被插入到原節(jié)點之前的舊行為需要調(diào)整斷言或邏輯同時確認目標運行環(huán)境已不再需要兼容 IE/舊版 Edge。使用Focus.focus且有滾動副作用困擾直接升級到 9.3.0傳第二個參數(shù)true即可禁止聚焦時滾動。處理不可編輯內(nèi)容的光標定位確保使用 9.2.0配合 8.0.0 引入的ContentEditable模塊統(tǒng)一管理可編輯狀態(tài)。九、總結(jié)從 8.0.0 到 9.3.0Sugar 的演進脈絡(luò)清晰可循一方面持續(xù)擴充能力ContentEditable模塊、Ready.image/Ready.video異步加載、parentElement、setOptions系列、尺寸測量 API另一方面不斷打磨健壯性與可用性nodeType前置校驗、空 class 清理、Firefox visualViewport 規(guī)避、preventScroll聚焦、光標位置判定增強同時通過破壞性版本8.0.0、9.0.0果斷收縮瀏覽器支持面并重構(gòu)語義不清晰的 APIReady.document更名、插入順序統(tǒng)一為后插。這些變更的源碼與測試均可直接在倉庫中查閱核心實現(xiàn)在 modules/sugar/src/main/ts/ephox/sugar/api瀏覽器行為驗證可參考 FocusTest.ts 等測試文件。對于 TinyMCE 的二次開發(fā)者或 Sugar 的直接使用者而言理解這些版本差異是在升級過程中避免回歸、并充分發(fā)揮 Sugar 能力的關(guān)鍵?!久赓M下載鏈接】tinymceThe worlds #1 JavaScript library for rich text editing. Available for React, Vue and Angular項目地址: https://gitcode.com/gh_mirrors/ti/tinymce創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考