Elementor Shapes 模块解析:SVG 路径如何驱动 Text Path(曲线路径文字)Widget
【免费下载链接】elementorThe most advanced frontend drag & drop page builder. Create high-end, pixel perfect websites at record speeds. Any theme, any page, any design.项目地址: https://gitcode.com/GitHub_Trending/el/elementor
本文以 Elementor 仓库中 Shapes 模块说明文档 为核心,结合 modules/shapes/module.php 的模块入口实现,深入讲解该模块如何以 SVG 形状作为 Widget 的核心特性:内置 6 种 SVG 路径资源的组织方式、Text Path Widget 的完整控制项配置、前端 JS 处理器如何通过fetch+ DOMPurify 动态注入 SVG 并构建<textPath>元素,以及 RTL 文字方向在浏览器间的兼容处理策略。读完后你将理解 Elementor 中"PHP 渲染骨架 + JS 渲染内容"这一典型协作模式,并掌握扩展自定义 SVG 路径的机制。
一、模块定位:Shapes 是 SVG 形状类 Widget 的聚合入口
模块文档 对 Shapes 模块的定义是:
This class is the main shapes module of Elementor, and it's responsible for all of the widgets which uses SVG shapes as their main feature. (该类是 Elementor 的主 shapes 模块,负责所有以 SVG 形状作为主要功能的 widget。)
当前归属该模块的 Widget 只有一个:Text Path——允许用户把文字放置在 SVG 路径(圆形、直线、波浪、椭圆、螺旋等)上。
从 modules/shapes/module.php 的源码结构看,Module类继承自Elementor\Core\Base\Module,共承担四类职责:
1. 前端样式注册。构造函数中通过钩子挂载样式注册逻辑(module.php 第 11-15 行):
public function __construct() { parent::__construct(); add_action( 'elementor/frontend/after_register_styles', [ $this, 'register_styles' ] ); }register_styles()注册了widget-text-path这条样式句柄(module.php 第 25-32 行),依赖elementor-frontend,版本号取ELEMENTOR_VERSION以便缓存失效。源码注释说明了构建时行为:Elementor 在构建期将/modules/shapes/assets/scss/frontend.scss编译为/assets/css/widget-shapes.min.css,而 modules/shapes/assets/scss/widgets/text-path.scss 即为该产物中 Text Path 部分的设计变量实现(见后文第四节)。
2. 内置路径目录。静态方法get_paths()返回可翻译的内置 SVG 形状列表(module.php 第 41-56 行):
| 键名 | 显示名 | 对应资源文件 |
|---|---|---|
wave | Wave(波浪) | assets/svg-paths/wave.svg |
arc | Arc(圆弧) | assets/svg-paths/arc.svg |
circle | Circle(圆形) | assets/svg-paths/circle.svg |
line | Line(直线) | assets/svg-paths/line.svg |
oval | Oval(椭圆) | assets/svg-paths/oval.svg |
spiral | Spiral(螺旋) | assets/svg-paths/spiral.svg |
当$add_custom参数为true(默认)时,列表末尾追加custom(Custom,自定义 SVG 上传)选项。仓库中 assets/svg-paths/ 目录下恰好存在与上表一一对应的 6 个 SVG 文件,印证了该目录即内置路径的物理来源。
3. 路径 URL 解析。get_path_url()负责把路径名转换为静态资源 URL(module.php 第 65-67 行):
public static function get_path_url( $path ) { return ELEMENTOR_ASSETS_URL . 'svg-paths/' . $path . '.svg'; }即拼接ELEMENTOR_ASSETS_URL常量、svg-paths/目录与<path>.svg文件名。这个 URL 最终会写入 Widget 的data-url属性,供前端 JS 抓取。
4. Widget 挂载。get_widgets()返回[ 'TextPath' ](module.php 第 74-78 行),get_name()返回模块名shapes。这也解释了 Text Path 源码 中get_group_name()返回'shapes'的设计——Widget 面板里按模块分组展示。
二、Text Path Widget:PHP 端只输出"骨架"
modules/shapes/widgets/text-path.php 中的TextPath类是模块文档中列出的核心 Widget。其关键设计是PHP 渲染容器属性,JS 渲染实际文字——widget 文档 在"Known Issues"部分明确提示:
The widget and the SVG markups are rendered in PHP, but the text itself is being rendered using JS.
2.1 渲染逻辑
render()方法(text-path.php 第 677-705 行)展示了完整的数据流:
- 计算路径 URL:若
path设置为custom,则取上传的 SVG 附件 URL(wp_get_attachment_url);否则调用Shapes_Module::get_path_url()获取内置路径 URL; - 剥离协议头:
preg_replace( '/^https?:/i', '', $path_url )移除http:/https:前缀,目的是防止混合内容(Mixed Content)错误,使 SVG 请求始终相对当前协议发起; - 输出空容器并携带三个 data 属性:
$this->add_render_attribute( 'text_path', [ 'class' => 'e-text-path', 'data-text' => htmlentities( esc_attr( $settings['text'] ) ), 'data-url' => esc_url( $path_url ), 'data-link-url' => esc_url( $settings['link']['url'] ?? '' ), ] ); // 若设置了 hover 动画,追加 elementor-animation-{name} 类 ?> <div <?php $this->print_render_attribute_string( 'text_path' ); ?>></div>注意最终输出是一个空<div>——文字内容(data-text)、SVG 来源(data-url)、链接(data-link-url)全部以 data 属性形式暂存,等待前端处理器消费。这正是"骨架在 PHP、血肉在 JS"模式的具体体现。
此外两个细节值得注意:get_style_depends()声明了widget-text-path样式依赖(text-path.php 第 88-90 行),保证前端加载该 Widget 时拉取第一节注册的样式;has_widget_inner_wrapper()则根据e_optimized_markup实验特性开关决定是否包裹 inner wrapper(text-path.php 第 92-94 行)。
2.2 内容 Tab 控制项
register_content_tab()(text-path.php 第 99-231 行)注册的控制项及其参数:
| 控制项 | 类型 | 关键配置 | 说明 |
|---|---|---|---|
text | TEXT | 默认Add Your Curvy Text Here,frontend_available: true,render_type: none,支持动态标签 | 路径上的文字;render_type: none意味着不直接参与静态渲染,交由 JS 处理 |
path | SELECT | 选项来自Shapes_Module::get_paths(),默认wave | 路径类型下拉框 |
custom_path | MEDIA | media_types: ['svg'],仅在path => 'custom'时显示 | 上传自定义 SVG |
link | URL | frontend_available: true,支持动态标签 | 为文字附加链接 |
align | CHOOSE(响应式) | 输出 CSS 变量--alignment: {{VALUE}},可前端编辑 | 左/中/右对齐 |
text_path_direction | SELECT | 默认空(Default),可选rtl/ltr,输出--direction: {{VALUE}} | 文字书写方向,默认跟随站点方向 |
show_path | SWITCHER | return_value为常量DEFAULT_PATH_FILL = '#E8178A' | 开启后输出--path-stroke并设--path-fill: transparent,让路径本身可见 |
2.3 样式 Tab 控制项
register_style_tab()(text-path.php 第 236-656 行)分为两大区块,其设计思路是几乎所有值都落到 CSS 自定义属性上,由 SCSS 统一消费:
Text Path 区块(文字侧):
size(响应式 SLIDER):支持px / % / em / rem / vw单位;%范围 0-100(步进 10),px上限 800(步进 50),桌面/平板/手机默认均为 500,输出--width;rotation(响应式 SLIDER):支持deg / grad / rad / turn,输出--rotate;text_typography(Typography 组控件):可绑定全局排版变量(默认TYPOGRAPHY_TEXT),字号默认 20px;源码注释特别说明text_decoration不是继承属性,因此需要显式指定{{WRAPPER}} textPath选择器(text-path.php 第 325-331 行);text_stroke(Text Stroke 组控件):选择器直接指向{{WRAPPER}} textPath;word_spacing(响应式 SLIDER):px 范围 -20 到 20,em/rem 范围 -1 到 1,输出--word-spacing;start_point(SLIDER):仅%单位,范围 -100 到 100,步进 1,frontend_available: true+render_type: none——该值完全由前端处理器解释为<textPath>的startOffset;- Normal / Hover 子页签:分别控制
--text-color/--text-color-hover、悬停动画(HOVER_ANIMATION 控件,最终映射为elementor-animation-*类)与过渡时长(默认 0.3s)。
Path 区块(路径侧,仅在show_path开启时显示):
- Normal:
--path-fill(填充色)、描边色--stroke-color(默认#E8178A)、描边宽--stroke-width(默认 1px,px 上限 20,em/rem 上限 2); - Hover:
--path-fill-hover、--stroke-color-hover、--stroke-width-hover,以及描边过渡时长--stroke-transition(默认 0.3s)。
三、前端处理器:JS 端如何"生长"出文字
3.1 处理器挂载与懒加载
modules/shapes/assets/js/frontend/frontend.js 只有 7 行,核心是注册懒加载处理器:
elementorFrontend.elementsHandler.attachHandler( 'text-path', () => import( /* webpackChunkName: 'text-path' */ './handlers/text-path' ) );attachHandler将处理器与 Widget 名称text-path绑定;import()的动态导入使该处理器被打成独立 chunk,只有页面实际存在 Text Path 元素时才下载——这是 Elementor 前端资产按需加载的常规做法。
3.2 SVG 抓取与净化
处理器 text-path.js 的onInit()流程(第 31-46 行):
- 依据 wrapper 上的
data-id(经 DOMPurify 净化后)生成两个唯一 ID:e-path-{id}与e-text-path-{id}。唯一性很关键:SVG 的href="#pathId"引用是文档级查找,同页多个 Text Path 实例若 ID 冲突会互相串线; - 调用
fetchSVG()(第 53-68 行):
fetchSVG() { const { url } = this.elements.pathContainer.dataset; if ( ! url || ! url.endsWith( '.svg' ) ) { return Promise.reject( url ); } return fetch( url ) .then( ( res ) => res.text() ) .then( ( svg ) => { this.elements.pathContainer.innerHTML = DOMPurify.sanitize( svg ); this.elements = this.getDefaultElements(); } ); }注意两层防御:URL 必须真实存在且以.svg结尾(否则直接 reject,这是 handlers 文档 中"Known Issues"隐含的安全性边界);抓回的 SVG 文本必须经DOMPurify.sanitize()净化后再注入innerHTML,防止自定义 SVG 上传场景中携带脚本。
3.3 构建 textPath 与 startOffset
SVG 就位后,initTextPath()(第 139-157 行)执行:
attachIdToPath()优先选取带data-path-anchor属性的path元素,找不到则回退到 SVG 中第一个path,并把生成的pathId赋给它(第 128-132 行)——data-path-anchor机制允许自定义 SVG 作者显式指定"文字应贴合哪条路径";- 在 SVG 内追加
<text><textPath id="..." href="#pathId"></textPath></text>结构; - 随后调用
setOffset()与setText()。
setOffset()(第 78-88 行)把面板中的start_point值转换为 SVG 规范的startOffset百分比属性,且在 RTL 模式下做100 - offset的镜像换算:
setOffset( offset ) { if ( ! this.elements.textPath ) return; if ( this.isRTL() ) { offset = 100 - parseInt( offset ); } this.elements.textPath.setAttribute( 'startOffset', offset + '%' ); }onElementChange()(第 97-120 行)监听编辑器中的实时变更:start_point变化只重设 offset;text变化重写文字;text_path_direction变化则两者都刷新。
setText()(第 166-206 行)负责把纯文本包装为链接文字:若设置了链接,则构造<a href rel target>标签(外部链接target="_blank",nofollow 时rel="nofollow"),并经 DOMPurify 以ADD_ATTR: ['target']白名单净化——target默认不在 DOMPurify 允许列表内,这里显式放行。
3.4 RTL 兼容:一套"hacky"但务实的策略
handlers 文档 坦承这是模块里历史包袱最重的部分:
shouldReverseText()- We've had a lot of issues with RTL in this widget. Most of the browsers lose it when it comes to RTL text in SVG, and only Firefox worked properly... we had to overcome this issue using a hacky way - We reversed the text in any browser which is not Firefox.
源码实现的策略分三层(text-path.js 第 213-282 行):
- 判断是否需要反转(
shouldReverseText()):非 RTL 直接跳过;Firefox 原生支持 SVG RTL 文字,跳过;Chromium 系则检查版本——Chromium 96+ 已修复 SVG RTL 文字问题(源码引用了 Chromium 官方修复记录),故isFixedChromiumVersion()通过解析navigator.userAgent判断当前 Chrome/Chromium/Edge 主版本是否 ≥ 96(第 255-260 行); - 执行反转(
reverseToRTL()):用正则匹配 RTL 字符集并逐词反转顺序,同时给可见元素加aria-hidden,并额外保留一份隐藏可选择的原始文字克隆(-clone节点)用于可访问性——文档也承认"this isn't a perfect solution, and it uses some unreadable regex",并提示后续浏览器若都补齐 RTL 支持,此代码可简化; - 方向判定(
isRTL()):默认跟随站点方向elementorFrontend.config.is_rtl,若用户在面板显式选择了rtl/ltr则以面板值为准——与内容 Tab 中text_path_direction的 Default/RTL/LTR 三态设计完全对应。
四、样式层:CSS 自定义属性作为 PHP 与 SCSS 的契约
modules/shapes/assets/scss/widgets/text-path.scss 完整消费了第二节列出的所有变量,是理解"控制项 → CSS 变量 → 视觉"链路的最佳入口:
.elementor-widget-text-path { font-size: 20px; text-align: var( --alignment, start ); svg { width: var( --width ); max-width: 100%; height: auto; overflow: visible; word-spacing: var( --word-spacing ); transform: rotate( var( --rotate, 0 ) ) scaleX( var( --scale-x, 1 ) ) scaleY( var( --scale-y, 1 ) ); path { vector-effect: non-scaling-stroke; /* 防止 SVG 缩放时描边粗细随之缩放 */ fill: var( --path-fill, transparent ); stroke: var( --stroke-color, transparent ); stroke-width: var( --stroke-width, 1px ); transition: var( --stroke-transition ) stroke, var( --stroke-transition ) fill; } &:hover { path { --path-fill: var( --path-fill-hover ); --stroke-color: var( --stroke-color-hover ); --stroke-width: var( --stroke-width-hover ); } } text { --fill: var( --text-color ); fill: var( --fill ); direction: var( --direction ); transition: var( --transition ) stroke, var( --transition ) stroke-width, var( --transition ) fill; &:hover { --color: var( --text-color-hover, var( --text-color ) ); --fill: var( --color ); color: var( --color ); } } } }三个工程细节值得提炼:
- 变量回写技巧:hover 态不改
fill/stroke本身,而是覆写--path-fill等上游变量,让同一声明在正常/悬停两态间平滑切换,transition因此始终对stroke/fill生效; vector-effect: non-scaling-stroke:SVG 尺寸由--width控制(可在 0-800px 间大幅缩放),该属性确保描边粗细不随图形缩放而失真;--scale-x/--scale-y的预留:SCSS 中出现了控制项里未注册的--scale-x/--scale-y变量(默认 1),从源码结构看是预留的扩展位,当前面板无对应控件。
五、数据流总览
把上述证据串起来,Text Path 一条完整的渲染链路是:
面板控制项(text-path.php) ├─ PHP render():输出 <div class="e-text-path" contenteditable="false">【免费下载链接】elementorThe most advanced frontend drag & drop page builder. Create high-end, pixel perfect websites at record speeds. Any theme, any page, any design.
项目地址: https://gitcode.com/GitHub_Trending/el/elementor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考