资讯动态

TinyMCE Sugar 9.3.0 版本演进全解:DOM 封装库的核心 API 变更与源码深度解析

发布时间:2026/9/21 18:16:53 来源:尧图企业网站定制
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 提供了类型安全、函数式的封装。本文基于仓库内 modules/sugar/CHANGELOG.md 与 .changes/sugar/9.3.0.md 的版本记录系统梳理 Sugar 从 8.0.0 到 9.3.0 的关键 API 演进包括Focus.focus的防滚动聚焦、Awareness.isCursorPosition对contenteditablefalse的支持、Ready.image异步资源加载、Remove.unwrap与Replication.mutate插入顺序调整以及ContentEditable新模块的引入。读完本文你将理解这些变更背后的设计动机与源码实现并掌握在 TinyMCE 生态中正确使用这些 API 的实战方法。一、Sugar 库在 TinyMCE 生态中的定位Sugar位于 modules/sugar是 TinyMCE 底层工具链的一部分同 Katamari函数式工具集、Sand跨浏览器平台检测、AlloyUI 组件框架等模块协同工作。它以SugarElementT这一轻量包装结构为核心——内部仅持有原生 DOM 节点的引用通过函数式 API 完成对节点、属性、样式、事件、选区、尺寸等领域的操作。Sugar 的源码目录结构清晰地划分了职责领域见 modules/sugar/src/main/ts/ephox/sugar/apidom/DOM 操作如Focus、Remove、Replication、Insertevents/事件处理与就绪检测如DomEvent、Readynode/节点类型判断如SugarNode、SugarElementproperties/属性与样式如Attribute、Class、ContentEditablesearch/遍历与查询如Traverse、SelectorFindselection/选区与光标位置如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参数2023-11-22变更内容9.3.0 版本的唯一变更是对Focus.focus函数的改进TheFocus.focusfunction now takes an additionalpreventScrollparameter to allow focus on an element without scrolling.即Focus.focus现在接受一个额外的preventScroll参数允许在不滚动页面的情况下将焦点赋予元素。源码实现查看 Focus.tsconst focus (element: SugarElementHTMLElement, preventScroll: boolean false): void element.dom.focus({ preventScroll });实现非常简洁Sugar 将preventScroll直接透传给原生HTMLElement.focus()的 options 对象。默认值为false因此这是一个完全向后兼容的增强——现有调用Focus.focus(element)的代码行为不变仍会触发滚动。实战用法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模块还提供以下相关函数Focus.ts函数说明focus(element, preventScroll?)聚焦指定元素blur(element)使元素失焦hasFocus(element)判断元素是否持有焦点通过root.activeElement比对active(root?)返回当前焦点元素OptionalSugarElementT支持传入 ShadowRootsearch(element)查找元素内部已聚焦的后代优先于:focus选择器不依赖键盘焦点状态focusInside(element)若元素内部尚无焦点则聚焦元素本身其中active与search均通过SugarShadowDom.getRootNode支持 Shadow DOM 场景。仓库中的测试 FocusTest.ts 覆盖了普通文档与 ShadowRoot 两种环境下active、search、hasFocus、focusInside的行为例如验证 ShadowRoot 的 activeElement 是内部输入框、而 Document 的 activeElement 是 shadow host。三、9.2.0Awareness.isCursorPosition支持contenteditablefalse2023-03-15变更内容Awareness.isCursorPositionAPI now returnstruefor passedcontenteditablefalseelements.即Awareness.isCursorPosition现在对contenteditablefalse的元素返回true。这意味着非可编辑元素也可以成为合法的光标停靠位置这对 TinyMCE 中处理图片、嵌入对象等不可编辑内容的光标定位至关重要。源码实现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判断一个节点是否可以作为光标位置共三种情况非空文本节点文本内容去除空白后非空或包含nbsp;Unicode.nbsp见isTextNodeWithCursorPosition固有光标元素img与brcontenteditablefalse的 HTML 元素9.2.0 新增。Awareness模块还配套提供getEnd、isEnd、isStart等函数用于计算元素的光标边界文本节点取其字符长度img固定为 1其余取子节点数。四、9.1.0SugarNode.isHTMLElement增加nodeType前置校验2022-09-08变更内容TheSugarNode.isHTMLElementfunction now ensures thenodeTypeis1before checking the prototypes.即SugarNode.isHTMLElement在检查原型链之前先确保nodeType 1元素节点。源码实现SugarNode.tsconst isHTMLElement (element: SugarElementNode): element is SugarElementHTMLElement isElement(element) SandHTMLElement.isPrototypeOf(element.dom);其中isElement是isTypeElement(NodeTypes.ELEMENT)即校验nodeType 1。这一前置检查避免了对文本节点、注释节点等非元素节点调用SandHTMLElement.isPrototypeOf时可能出现的误判或性能开销使类型守卫更加严谨。isHTMLElement与isTag、isText、isDocument等共同构成了 Sugar 基于nodeType的节点类型判别体系。五、9.0.0破坏性变更与跨浏览器支持收缩2022-03-039.0.0 是这一系列中变更最密集的版本包含新增、变更、移除、修复四类改动。5.1 新增Ready.image图片加载完成后再继续NewReady.imagefunction that returns a promise which will not resolve until the image element has loaded. Errors trigger promise rejection.Ready.ts 的实现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(); } });实现要点通过DomEvent.bind同时监听load与error事件若图片已经缓存完成image.dom.complete true立即 resolve避免死等加载失败时 reject 并携带图片src信息方便排查无论成功失败都会解绑监听器避免内存泄漏。实战示例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根据document.readyState判断若已是complete或interactive则立即执行回调否则监听DOMContentLoaded后执行一次并解绑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插入顺序调整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.这两处调整将解包/替换操作中原有的前插before改为后插after。从 DOM 遍历语义看新顺序更符合直觉子节点或替换节点紧跟在原节点之后保持后续兄弟节点的相对位置稳定。查看 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的典型应用是去掉包裹层例如将bspantext/span/b中的b去掉子节点span会被移到b之后即原位置再删除b本身。mutate则用于原地换标签先创建一个同属性新标签shallowAs会通过Attribute.clone复制全部属性插入到原节点之后把原节点的所有子节点搬入新节点最后删除原节点——例如在表格单元格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 的基础工具库Optional、Arr、Obj等均来自ephox/katamari。9.0.0 同步升级到 Katamari 9.0其OptionalAPI 的破坏性变更如getOrDie、fold等签名调整会传导到 Sugar 的公开接口因此本次也属于破坏性版本。同时Sugar 正式移除对 IE 与旧版 EdgeEdgeHTML 内核的支持后续代码可以依赖现代浏览器 API如classList、Promise、Shadow DOM而无需降级兼容——这一点在后续 11.0.0 的 CHANGELOG 中也有呼应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在操作完成后都会调用cleanClass当classList长度为 0 时通过Attribute.remove彻底移除class属性。toggler工厂函数的off回调也做了同样处理。这样生成的 DOM 更干净也避免了一些对空 class 属性敏感的 CSS 选择器或序列化场景出现问题。六、8.1.0遍历、批量属性与尺寸 API 扩充2021-10-116.1 新增Traverse.parentElementparentElement返回元素的父元素OptionalSugarElementHTMLElement与parent/parentNode的区别在于它基于原生element.parentElement只会命中元素节点跳过文本节点等非元素父节点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)时设置属性为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); }); }); };实战示例——根据条件设置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 被设为 truetitle 被移除与之对应的底层rawSet仅接受 string / boolean / number 三类值其余类型会console.error并抛错避免把非法值写入 DOM。Css.setOptions语义相同值为Optionalstring用于按条件设置或清除内联样式Css.ts。6.3 新增Width.getInner/Height.getInner与getRuntime8.1.0 为尺寸 API 增加了四个函数Width.getInner/Height.getInner获取元素的内容区尺寸不含 padding/border对应clientWidth/clientHeightWidth.getRuntime/Height.getRuntime获取运行时实际渲染尺寸。实现位于 Width.ts 与 Height.ts内部委托给 impl/RuntimeSize.ts 完成具体测量。这让调用方可以按需选择文档声明的 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); } };源码注释引用了内部问题号 TINY-7984Firefox 91 在position: fixed场景下visualViewport.pageTop返回值不正确因此 Sugar 对 Firefox 回退到documentElement.clientWidth/clientHeight加滚动偏移的方式计算边界getBounds。getBounds中还对 iOS 的pageLeft/pageTop与滚动位置取了最大值Math.max以规避scrollIntoView()不更新 pageTop 的兼容性问题。七、8.0.0新增ContentEditable模块2021-08-26变更内容Added newContentEditablemodule to determine if an HTML element is content editable.8.0.0 引入了独立的ContentEditable模块集中处理元素是否可编辑的判断与设置此前这类逻辑散落在各处。源码实现ContentEditable.ts 提供五个函数函数说明get(element)返回元素是否可编辑布尔值getRaw(element)返回原生element.dom.contentEditable字符串true/false/inheritset(element, editable)设置contentEditable为true或falseclosest(target)沿祖先链查找最近的[contenteditable]元素isEditable(element, assumeEditable?)综合判断可编辑性isEditable的实现值得注意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会综合继承状态元素尚未挂载到文档时isContentEditable不可靠改为向上查找最近的[contenteditable]祖先判断其原始属性是否为true若找不到任何祖先则回退到assumeEditable参数默认false。与 9.2.0 变更的呼应8.0.0 的ContentEditable模块与 9.2.0 的Awareness.isCursorPosition变更形成了完整的能力闭环ContentEditable负责判断和设置可编辑性Awareness.isCursorPosition负责在光标定位时承认contenteditablefalse元素是合法的光标停靠点。两者共同支撑 TinyMCE 在富文本中处理不可编辑内容如图片、嵌入对象时的选区与光标逻辑。升级注意8.0.0 同样声明升级了 Katamari 8.0OptionalAPI 存在破坏性变更这意味着使用 Sugar 8.0.0 时需同步升级依赖的 Katamari 版本。八、版本演进速查表与升级建议版本日期类型核心内容9.3.02023-11-22ImprovedFocus.focus新增preventScroll参数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重点关注Ready.execute→Ready.document的重命名以及Remove.unwrap、Replication.mutate插入顺序变化对 DOM 结果的潜在影响若你的代码依赖子节点被插入到原节点之前的旧行为需要调整断言或逻辑同时确认目标运行环境已不再需要兼容 IE/旧版 Edge。使用Focus.focus且有滚动副作用困扰直接升级到 9.3.0传第二个参数true即可禁止聚焦时滚动。处理不可编辑内容的光标定位确保使用 9.2.0配合 8.0.0 引入的ContentEditable模块统一管理可编辑状态。九、总结从 8.0.0 到 9.3.0Sugar 的演进脉络清晰可循一方面持续扩充能力ContentEditable模块、Ready.image/Ready.video异步加载、parentElement、setOptions系列、尺寸测量 API另一方面不断打磨健壮性与可用性nodeType前置校验、空 class 清理、Firefox visualViewport 规避、preventScroll聚焦、光标位置判定增强同时通过破坏性版本8.0.0、9.0.0果断收缩浏览器支持面并重构语义不清晰的 APIReady.document更名、插入顺序统一为后插。这些变更的源码与测试均可直接在仓库中查阅核心实现在 modules/sugar/src/main/ts/ephox/sugar/api浏览器行为验证可参考 FocusTest.ts 等测试文件。对于 TinyMCE 的二次开发者或 Sugar 的直接使用者而言理解这些版本差异是在升级过程中避免回归、并充分发挥 Sugar 能力的关键。【免费下载链接】tinymceThe worlds #1 JavaScript library for rich text editing. Available for React, Vue and Angular项目地址: https://gitcode.com/gh_mirrors/ti/tinymce创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价