ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Elementor Nested-Elements 模块深度解析:搭建「Widget 内嵌 Widget」的编辑器基础设施

Elementor Nested-Elements 模块深度解析:搭建「Widget 内嵌 Widget」的编辑器基础设施 Elementor Nested-Elements 模块深度解析搭建「Widget 内嵌 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/elementorNested-Elements 是 Elementor 中一个默认启用且标记为 Stable 的实验模块它为 Tabs、Accordion 这类「容器型 Widget」提供了在 Widget 内部再嵌套子元素Container的完整编辑器基础设施包括命令式数据钩子、Model/View 基类与自定义 Repeater 控件。读完本篇你可以完整理解该模块从 PHP 后端注册、JS 加载链到钩子生命周期的实现机制并知道如何基于Widget_Nested_Base与nested-elements-repeater控件扩展自己的嵌套元素 Widget。模块定位实验配置与 PHP 端注册模块入口是 modules/nested-elements/module.php。Module类通过get_experimental_data()声明了自己的实验属性return [ name nested-elements, title esc_html__( Nested Elements, elementor ), release_status Experiments_Manager::RELEASE_STATUS_STABLE, default Experiments_Manager::STATE_ACTIVE, mutable false, hidden true, dependencies [ container, ], ];对应 module.php关键信息可以读出四点实验状态为Stable默认Activemutable false且hidden true——即用户既不能关闭它、也看不到它的开关它是 Tabs/Accordion 等嵌套 Widget 的底层依赖它依赖container实验模块因为嵌套进来的子元素就是 Flexbox Container整个机制建立在全局容器Flexbox Containers能力之上构造函数中通过elementor/controls/register钩子注册了自定义控件Control_Nested_Repeater即编辑器面板中的nested-elements-repeater控件通过elementor/editor/before_enqueue_scripts钩子引入编辑器 JS 资产且声明依赖elementor-common对应 module.php。后端基类Widget_Nested_Basemodules/nested-elements/base/widget-nested-base.php 定义了所有「支持嵌套」的 Widget 后端基类。它是理解整个模块配置契约的核心因为它决定了哪些默认配置会被发送给编辑器。两个必须实现的抽象方法abstract protected function get_default_children_elements(); // 默认子元素结构 abstract protected function get_default_repeater_title_setting_key(); // 子项标题对应的 settings keyget_default_children_elements()返回该 Widget 首次创建时应自动生成的子元素数组例如一个_title为Tab #1的 containerget_default_repeater_title_setting_key()返回 Repeater 控件中「子项标题」字段对应的 settings key例如 Tabs 用的是tab_title前端会依据它生成Tab #1、Tab #2这样的默认标题。可选重写的方法及其默认值方法默认值作用get_default_children_title()Item #%d导航器/子元素默认标题模板%d为索引占位符widget-nested-base.phpget_default_children_placeholder_selector()追加到视图末尾子元素在 Widget 模板中的插入位置 CSS 选择器get_default_children_container_placeholder_selector()子 container 的逐位插入选择器用于子元素不位于模板末尾的场景is_dynamic_content()false是否动态内容发送给付端编辑器的defaults配置get_initial_config()在父类配置之上合并了一组关键字段widget-nested-base.phpdefaults [ elements $this-get_default_children_elements(), elements_title $this-get_default_children_title(), elements_placeholder_selector $this-get_default_children_placeholder_selector(), child_container_placeholder_selector $this-get_default_children_container_placeholder_selector(), repeater_title_setting $this-get_default_repeater_title_setting_key(), ], support_nesting true,其中support_nesting true是前端判断「该 Widget 是否支持嵌套」的唯一依据后文isWidgetSupportNesting直接读取elementor.widgetsCache[ widgetType ].support_nesting。另外该类还提供_get_default_child_type()把允许的子元素类型解析为 elements_manager 中的元素类型使 Widget 在结构上「允许被嵌套」get_raw_data()导出时递归携带每个子元素的原始数据elements数组保证嵌套结构可完整序列化print_child( $index )按索引渲染第 N 个子元素供子模板调用print_template()当 Widget 配置了support_improved_repeaters时额外输出一段text/html的单项子模板脚本content_template_single_repeater_item供前端在 Repeater 增删项时增量渲染而不是整表重渲widget-nested-base.php。基于以上契约一个最小嵌套 Widget 的 PHP 骨架大致是示例非仓库内文件class Widget_My_Nested extends \Elementor\Modules\NestedElements\Base\Widget_Nested_Base { protected function get_default_children_elements() { return [ [ elType container, settings [ _title __( Item #1, my-plugin ) ], ] ]; } protected function get_default_repeater_title_setting_key() { return item_title; } }编辑器 JS 加载链index.js → module.js → component.js编辑器侧入口是 modules/nested-elements/assets/js/editor/index.js它解决的是「依赖模块需要等待本模块就绪」的时序问题elementorCommon.elements.$window.on( elementor:init-components, () { // Put promise of loading so other modules can use/await it. elementor.modules.nestedElements import( ../editor/module ); elementor.modules.nestedElements.then( ( module ) { elementor.modules.nestedElements new module.default; // ... 继续动态加载 NestedElementBase 类型与 NestedView } ); } );流程分三步先把import( ../editor/module )返回的Promise挂到全局elementor.modules.nestedElements上其他模块如 Tabs可以await它模块加载完成后将 Promise替换为实例本身new module.default见 module.js——NestedElementsModule构造函数里执行$e.components.register( new Component() )即注册nested-elements组件随后继续加载元素类型基类 nested-element-types-base.jsNestedElementTypesBase其getModel()返回nested-elements/nested-repeater组件导出的NestedModelBase并在全部就绪后向窗口派发elementor/nested-element-type-loaded事件作为「嵌套元素体系可用」的最终信号。component.js 定义了nested-elements命名空间的组件在registerAPI()中注册子组件$e.components.register( new NestedRepeaterComponent() );核心组件nested-elements/nested-repeaternested-repeater/component.js 是整个模块的功能中枢命名空间为nested-elements/nested-repeater做四件事exports { NestedModelBase, NestedViewBase, }; registerAPI() { super.registerAPI(); elementor.addControlView( nested-elements-repeater, RepeaterControl ); } defaultHooks() { return this.importHooks( hooks ); }导出NestedModelBase/NestedViewBase两个基类供具体嵌套 Widget 的子类型继承$e.components.get(nested-elements/nested-repeater).exports注册自定义控件视图nested-elements-repeater对应后端Control_Nested_Repeater通过importHooks( hooks )批量注册数据/UI 钩子。对应的后端控件在 control-nested-repeater.php它仅把控件类型从repeater换成nested-elements-repeaterconst CONTROL_TYPE nested-elements-repeater由 JS 端控件视图接管实际行为——「PHP 定类型、JS 定行为」的分工模式。钩子系统从基础条件到容器生命周期基础钩子条件base.js所有数据钩子都继承 hooks/data/base.js并继承$e.modules.hookData.After命令执行后触发。当前源码中的两个基础条件为export default class Base extends $e.modules.hookData.After { getContainerType() { return widget; } getConditions( args ) { return isWidgetSupportNesting( args.container.model.get( widgetType ) ); } }即目标容器必须是widget类型且该 Widget 类型support_nesting为 true。isWidgetSupportNesting的实现见 utils.js——直接读取elementor.widgetsCache[ widgetType ].support_nesting而这个字段正是上一节 PHP 端get_initial_config()写入的。说明文档早期版本中该条件写作$e.components.get( nested-elements ).isWidgetSupportNesting( ... )现仓库已收敛为从 utils 导入的独立函数。创建document/repeater/insertnested-repeater-create-container.js 是「Repeater 加一行 → Widget 内多一个子 Container」的核心getCommand() { return document/repeater/insert; } getConditions( args ) { // 只在命令被直接调用时处理排除 duplicate/move 等间接触发的场景 const isCommandCalledDirectly $e.commands.isCurrentFirstTrace( this.getCommand() ); return super.getConditions( args ) isCommandCalledDirectly; } apply( { container, name } ) { const index container.repeaters[ name ].children.length; $e.run( document/elements/create, { container, model: { elType: container, isLocked: true, _title: extractNestedItemTitle( container, index ), }, options: { edit: false }, // 不抢占焦点 } ); // 若 Widget 支持原子 Repeater再向预览窗口派发 CustomEvent 同步前端 }三个设计点值得注意幂等防护isCurrentFirstTrace保证只有「用户直接点 号」这一条链路会创建容器而由duplicate、move等命令间接触发的 insert 不会重复建容器那些场景由各自的钩子负责标题调整在此完成新容器的_title由extractNestedItemTitle( container, index )生成utils.js 中用后端下发的elements_title模板做sprintf( title, index )所以导航器里看到的是Tab #1、Tab #2而不是Container早期文档中列出的独立文件hooks/data/document/elements/create/nested-repeater-adjust-container-titles.js在当前仓库路径下已不存在该职责已并入此钩子容器被加锁isLocked: true只有从 Repeater 生成、带锁的 container 才能成为嵌套 Widget 的合法子元素见下文isValidChild。删除与复制remove / duplicate / movenested-repeater-remove-container.js监听document/repeater/remove通过findChildContainerOrFail( container, index )utils.js找不到即抛错定位到对应子 container执行document/elements/deleteforce: true删除同样带isCurrentFirstTrace防护并在需要时派发elementor/nested-container/atomic-repeater事件action.type: removenested-repeater-duplicate-container.js监听document/repeater/duplicate复制对应子 containerdocument/elements/duplicate并调用sortViewsByModels()重排子视图索引以与新模型对齐仓库中还包含 nested-repeater-move-container.js处理document/repeater/move时子容器随 Repeater 行迁移的同步逻辑。从这套钩子覆盖 insert/remove/duplicate/move 四个命令可以看出模块的设计目标是让「Repeater 行的增删改」与「子 container 的生命周期」严格一一镜像。UI 钩子聚焦当前正在编辑的容器nested-repeater-focus-current-edited-container.js 是唯一的 UI 钩子挂在panel/editor/open命令之后。解决的问题是嵌套 Tabs 可能有任意深度当你在深层容器上双击打开面板时需要把祖先链上每一层的 Repeater 依次切换到「正在编辑的那一行」否则用户看到的还是第一行。const NAVIGATION_DEPTH_SENSITIVITY_TIMEOUT 250; apply() { let depth 1; this.navigationMap.forEach( ( { container, index } ) { setTimeout( () { $e.run( document/repeater/select, { container, index: index, options: { useHistory: false }, // 不产生历史记录 } ); }, NAVIGATION_DEPTH_SENSITIVITY_TIMEOUT * depth ); depth; } ); }实现要点getConditions先排除元素创建场景isCurrentFirstTrace( document/elements/create )时不处理再沿getParentAncestry()向上遍历筛出所有父级为 widget 的子 container构建navigationMap当前激活行activeItemIndex与目标行 index 不一致的项才需要切换切换动作就是逐层执行document/repeater/select命令——这正是面板 Repeater 控件选中行所走的专用命令用250ms × depth的阶梯延迟执行注释中说明原因嵌套层级越深若无延迟连续的 select 执行过快用户「根本看不到」导航器逐层聚焦的过程。模型层NestedModelBasenested-model-base.js 是嵌套 Widget 子类型 Model 的基类负责两件事1. 合法子元素判定isValidChildisValidChild( childModel ) { const parentElType this.get( elType ), childElType childModel.get( elType ); return container childElType widget parentElType isWidgetSupportNesting( this.get( widgetType ) ) // 只有从 Repeater 创建的、带锁的 container 才能进入 Tabs 类 Widget childModel.get( isLocked ); }子元素必须是container、父元素必须是支持嵌套的widget且子 container 必须isLocked——这正好与创建钩子里isLocked: true的设定闭环阻止了用户手动拖拽普通容器进嵌套 Widget。2. 默认子元素创建initialize()中设置supportRepeaterChildren: true并在判定为「新建元素」elements为空且当前命令链包含document/elements/create时调用onElementCreate()其内容是把后端defaults.elements逐条转换为子模型分配唯一 id、补全settings/elements字段并强制isLocked truenested-model-base.js。这就是「拖入一个 Tabs Widget编辑器里自动出现 Tab #1」的来源。视图层NestedViewBasenested-view-base.js 继承BaseWidget视图重写了子视图挂载逻辑getChildViewContainer( containerView, childView ) { const { elements_placeholder_selector: customSelector, child_container_placeholder_selector: childContainerSelector } this.model.config.defaults; if ( childView ! undefined childView._index ! undefined childContainerSelector ) { return containerView.$el.find( ${ childContainerSelector }:nth-child(${ childView._index 1 }) ); } if ( customSelector ) { return containerView.$el.find( customSelector ); } return super.getChildViewContainer( containerView, childView ); } getChildType() { return [ container ]; }它把 PHP 端defaults里的两个占位选择器真正用起来设置了child_container_placeholder_selector时子 container 按索引精确挂载到模板中的第 N 个占位节点适合子元素嵌在模板深处的 Widget否则若设置了elements_placeholder_selector全部子元素挂到该选择器指向的容器两者都为空基类默认时回退到默认行为——追加到元素视图末尾。getChildType()返回[ container ]即子视图只能是 container 类型。自定义 Repeater 控件nested-elements-repeatercontrols/repeater.js 继承elementor.modules.controls.Repeater是面板中管理子项标题的控件视图主要覆盖三个方法getDefaults() { const widgetContainer this.options.container, defaults widgetContainer.model.config.defaults, index widgetContainer.children.length 1; return { _id: , // 用后端下发的 repeater_title_setting key如 tab_title承载默认标题 [ defaults.repeater_title_setting ]: extractNestedItemTitle( widgetContainer, index ), }; } onChildviewClickDuplicate( childView ) { $e.run( document/repeater/duplicate, { container: this.options.container, name: this.model.get( name ), index: childView._index, } ); this.toggleMinRowsClass(); } updateActiveRow() { if ( ! this.currentEditableChild ) { return; } $e.run( document/repeater/select, { container: this.container, index: this.currentEditableChild.itemIndex, options: { useHistory: false }, } ); }getDefaults()新 Repeater 行的默认值不再走通用标题逻辑而是按defaults.repeater_title_setting对应 PHP 端get_default_repeater_title_setting_key()写入Tab #N式标题updateActiveRow()点击某一行时改走document/repeater/select命令而非内部状态切换从而让上面「聚焦当前编辑容器」的 UI 钩子链路生效onChildviewClickDuplicate()行上「复制」按钮同样改走document/repeater/duplicate命令className()还做了一个 CSS 类名归一化elementor-control-type-nested-elements-repeater复用repeater控件的样式。工具函数与消费方式utils.js 汇集了模块共享的纯函数isWidgetSupportNesting读support_nesting、isWidgetSupportAtomicRepeaters读support_improved_repeaters、shouldUseAtomicRepeaters两者与的关系决定是否向前端预览派发elementor/nested-container/atomic-repeater增量同步事件、extractNestedItemTitle标题模板格式化与findChildContainerOrFail按索引取子容器。「如何消费本模块」的官方指引指向 NestedTabs 模块文档——NestedTabs 是该基础设施之上构建的具体产品形态嵌套 Tabs Widget而 Accordion 等嵌套元素同理PHP 端继承Widget_Nested_Base实现两个抽象方法编辑器端让其子类型基于NestedElementTypesBase的 Model/View 基类注册其余的容器创建、删除、复制、导航器标题与逐层聚焦全部由本模块的命令钩子自动接管。参考文件清单分类文件PHPmodule.php · widget-nested-base.php · control-nested-repeater.php加载链index.js · module.js · component.js · nested-element-types-base.js组件nested-repeater/component.js数据钩子base.js · insert · remove · duplicate · moveUI 钩子focus-current-edited-container.jsModel / Viewnested-model-base.js · nested-view-base.js控件 / 工具repeater.js · utils.js需要说明本文文件清单以当前仓库实际结构为准与模块文档早期列出的目录树略有出入例如独立的nested-repeater-adjust-container-titles.js已并入创建钩子且仓库新增了duplicate/move两个数据钩子与views子目录。【免费下载链接】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),仅供参考
返回列表