☰
Moodle Action Menu 组件开发指南:下拉动作菜单的渲染、触发器定制与子面板(Subpanel)实现
2026/9/29 2:49:33 网站建设 项目流程
  • 教育
  • 后端
  • 前端

【免费下载链接】moodle

Moodle - the world's open source learning platform

项目地址:https://gitcode.com/gh_mirrors/mo/moodle
点击查看免费下载

Moodle 的 Action Menu(动作菜单)是一个可复用的输出组件,用于把一组操作以“下拉菜单 + 内联操作”的形式展示在页面上。它被广泛用于用户菜单(user menu)、课程管理菜单(course administration menu)与活动管理菜单(activity administration menu)等场景。读完本文,你将掌握如何用纯 PHP 渲染一个动作菜单、如何定制触发器(默认按钮、kebab 菜单、文本/图标触发器)、如何区分主操作(Primary)与次级操作(Secondary),以及如何通过subpanel挂载任意 renderable 内容(例如选择列表),并理解其底层模板与测试实现。

工作原理概述

Action Menu 本质上是一个输出组件(renderable + templatable),由 核心实现类 负责收集“主操作”与“次级操作”两类动作:

  • 主操作(primary actions):直接显示在触发器按钮旁边,作为即时可见的快捷动作;
  • 次级操作(secondary actions):收进触发器按钮打开的下拉菜单(dropdown)中,保持界面整洁。

在渲染层面,组件将数据导出给 action_menu.mustache 模板,再由 Bootstrap dropdown 交互(data-bs-toggle="dropdown")与 YUI/AMD 模块负责展开与键盘导航。整个流程完全可以在服务端用 PHP 完成:创建实例 → 配置触发器 → 添加条目 → 渲染输出。

该组件在 Moodle 4.3(issue MDL-78665 已废弃:该文件现在只抛出一个coding_exception异常,提示不应被任何组件手动引入;action_menu、action_menu_link、pix_icon等类已全部迁移到lib/classes/output/目录(下文有完整文件地图)。

源码文件地图

围绕 Action Menu,当前仓库中可以直接阅读的核心文件如下(均为仓库根目录相对路径):

文件作用
public/lib/classes/output/action_menu.php主类core\output\action_menu,管理主/次操作、触发器、对齐方式,并导出模板上下文
public/lib/classes/output/action_menu/link.php通用链接条目类link(旧名action_menu_link),$primary属性决定主/次归属
public/lib/classes/output/action_menu/link_primary.php主操作便捷类link_primary(旧名action_menu_link_primary)
public/lib/classes/output/action_menu/link_secondary.php次级操作便捷类link_secondary(旧名action_menu_link_secondary)
public/lib/classes/output/action_menu/subpanel.php子面板条目类subpanel,并自动class_alias到旧命名空间core\output\local\action_menu\subpanel
public/lib/classes/output/pix_icon.php图标输出类pix_icon,可作为主操作直接加入菜单
public/lib/classes/output/choicelist.php选择列表 renderable,常作为子面板内容使用
public/lib/templates/action_menu.mustache动作菜单主模板,渲染主操作区域与触发器
public/lib/templates/action_menu_trigger.mustache触发器模板(含下拉菜单容器),旧辅助模板之一
public/lib/templates/action_menu_link.mustache链接条目模板
public/lib/templates/action_menu_item.mustache通用条目包装模板
public/lib/templates/local/action_menu/subpanel.mustache子面板模板(新辅助模板目录)
public/admin/tool/componentlibrary/examples/actionmenu.php组件库(Component Library)中的可运行示例页
public/lib/tests/behat/action_menu_subpanel.feature子面板的 Behat 行为测试
public/lib/tests/behat/fixtures/action_menu_subpanel_output_testpage.php子面板测试专用页面

关于模板文件的位置约定:lib/templates/action_menu_*是旧版辅助 mustache 文件所在位置(如action_menu_trigger.mustache、action_menu_link.mustache、action_menu_item.mustache),而lib/templates/local/action_menu/*用于存放任何新的辅助模板(目前包含subpanel.mustache)。

渲染一个动作菜单

组件输出类可以完全在 PHP 中渲染一个动作菜单,标准步骤是:

  1. 创建一个action_menu实例(可带条目,也可先空着);
  2. (可选)设置菜单触发器;
  3. (可选)向菜单添加条目(如果创建时没有传入);
  4. 渲染菜单。

先看一个最基础的例子:

/** @var core_renderer $output*/ $output = $PAGE->get_renderer('core'); $menu = new action_menu(); // Add items. $menu->add(new action_menu_link( new moodle_url($PAGE->url, ['foo' => 'bar']), new pix_icon('t/emptystar', ''), 'Action link example', false )); echo $output->render($menu);

同一个例子,把条目直接传入构造函数:

/** @var core_renderer $output*/ $output = $PAGE->get_renderer('core'); $menu = new action_menu([ new action_menu_link( new moodle_url($PAGE->url, ['foo' => 'bar']), new pix_icon('t/emptystar', ''), 'Action link example', false ), ]); echo $output->render($menu);

从源码看,action_menu::__construct(array $actions = [])会为每个传入的 action 调用一次add()方法,因此两种写法完全等价。构造函数同时会初始化一组默认容器属性(见 action_menu.php 构造实现):

  • 菜单容器:id="action-menu-{instance}"、class="moodle-actionmenu"、data-enhance="moodle-core-actionmenu";
  • 主操作容器(menubar)与次级下拉菜单(menu)都有独立的 id 与 ARIA 属性(role="menu"、aria-labelledby等);
  • 下拉对齐默认使用dropdown-menu-end(即 Bootstrap 的右对齐)。

设置菜单触发器

默认情况下,动作菜单的触发器是一个使用t/edit_menu图标的按钮,并带有一个 caret(小箭头)指示符。不过类提供了丰富的方法,可以把它改成 kebab 菜单(竖排三点),甚至显示任意自定义内容。

示例:kebab 菜单

/** @var core_renderer $output*/ $output = $PAGE->get_renderer('core'); $menu = new action_menu(); $menu->set_kebab_trigger(get_string('edit'), $output); $menu->set_additional_classes('fields-actions');

源码实现中,set_kebab_trigger()(见 action_menu.php)会做以下事情:

  • 使用i/menu图标加上一段visually-hidden的可访问文本(文本缺省时使用get_string('actions'));
  • 触发器应用默认样式类btn btn-icon d-flex no-caret(常量DEFAULT_KEBAB_TRIGGER_CLASSES),因此 kebab 触发器本身不带 caret;
  • 在triggerattributes中写入title,提升可访问性。

set_additional_classes()则会把传入的类追加到菜单容器根节点的class上,方便前端定制样式。

自定义触发器:带文本标签

// This example displays an "Edit" label for the trigger. $menu = new action_menu(); $menu->set_menu_trigger(get_string('edit'));

set_menu_trigger($trigger, $extraclasses = '')是更底层的触发器设置方法:第一个参数是触发器内容(文本或已渲染的 HTML),第二个参数是额外的样式类。注意它与set_action_label()的区别:set_menu_trigger设置的是触发器按钮上“看得见”的内容,而set_action_label()设置的是按钮的可访问名称(accessible name)。

自定义触发器:带图标

当把菜单触发器渲染为纯图标按钮时,务必让图标以“装饰性图像”(decorative image)的方式渲染——如果使用pix_icon,请传入空的$alt参数:

$menu = new action_menu(); // Make sure the pix icon is rendered as a decorative image by passing an empty alt parameter. $icon = $output->pix_icon('t/edit', ''); $menu->set_menu_trigger($icon); $menu->set_action_label(get_string('edit'));

图标按钮的可访问名称应当设置在按钮元素本身,有两种推荐方式:

方式一:使用set_action_label()方法(见上方代码)。该名称会被写入触发器的aria-label属性。

方式二:在图标旁边追加一段视觉隐藏文本:

$menu = new action_menu(); // Make sure the pix icon is rendered as a decorative image by passing an empty alt parameter. $icon = $output->pix_icon('t/edit', ''); // Add a visually hidden text label for the trigger button. $icon .= html_writer::span(get_string('edit'), 'visually-hidden'); $menu->set_menu_trigger($icon);

从 pix_icon 构造实现 可以看到,当$alt为空字符串时,类会自动给图标加上aria-hidden="true",这正是“装饰性图标”语义的来源——屏幕阅读器会跳过该图标,而由按钮上的可访问文本承担语义。这也是为什么文档强调图标触发器要把可访问名称放在按钮元素内部,而不是依赖图标的 alt 文本。

移除 caret 符号

如果不需要 caret,可以通过给triggerextraclasses属性添加no-caret类来移除:

$menu->triggerextraclasses = 'no-caret';

在 action_menu_trigger.mustache 模板中,caret 只有在menutrigger存在时才会渲染({{#menutrigger}}<b class="caret"></b>{{/menutrigger}}),并且触发器根元素同时带有dropdown-toggle类(Bootstrap 的dropdown-toggle默认也会显示 caret,no-caret类用于抑制它)。set_menu_trigger()的第二个参数也可以直接传入额外类,等价于手动设置triggerextraclasses。

在组件库的 示例页 中,你还能看到更多触发器变体,例如:仅图标触发器(配合set_action_label(get_string('moremenu'))与triggerattributes = ['title' => ...])、仅图标 + 移除 caret、图标 + 可见文本、图标 + 视觉隐藏文本等。示例页顶部也特别提醒:动作菜单不适合在 iframe 中展示,你可能需要滚动才能看到菜单选项——这正是该示例在组件库文档中通过 iframe 嵌入时给出的使用注意。

添加菜单项:主操作与次级操作

条目可以在创建时以数组传入,也可以通过add()方法添加。根据传给add()的参数类型,条目会被放到两个不同的位置:

  • Primary items(主操作):显示在触发器按钮旁边,作为直接动作;
  • Secondary items(次级操作):显示在动作菜单的下拉菜单内部。

条目所属位置必须在添加之前确定。看下面的完整示例,它展示了添加主、次条目的不同写法:

// Primary items examples. $menu->add(new action_menu_link( new moodle_url($PAGE->url), new pix_icon('t/emptystar', ''), 'Action link example', true )); $menu->add(new action_menu_link_primary( $PAGE->url, new pix_icon('t/emptystar', ''), 'Action link example', )); // Secondary items examples. $menu->add(new action_menu_link( new moodle_url($PAGE->url), new pix_icon('t/emptystar', ''), 'Action link example', false )); $menu->add(new action_menu_link_secondary( $PAGE->url, new pix_icon('t/user', ''), 'Action link example', ));

从源码看,action_menu::add($action)(见 action_menu.php)的分发逻辑是:

  • 如果是subpanel实例 → 加入次级子面板;
  • 如果是action_link(action_menu_link是其子类)→ 依据其primary属性决定加入主操作还是次级操作;
  • 如果是pix_icon→ 作为主操作加入;
  • 其余类型(如纯字符串)→ 作为次级操作加入。

无论是主操作还是次级操作,源码都会为action_link/pix_icon类型的条目自动补上role="menuitem"与tabindex="-1"属性(见 add_primary_action/add_secondary_action),以保证键盘与屏幕阅读器的可操作性。

此外,action_menu类还提供了一些常用的布局/行为 API,可以在实际开发中按需调用:

  • set_menu_left():将下拉菜单改为左对齐(dropdown-menu-start),见 action_menu.php;
  • set_boundary(string $boundary):设置下拉溢出的约束边界,仅接受viewport、window、scrollParent三个值,否则抛出coding_exception,见 action_menu.php;
  • set_nowrap_on_items($value = true):让菜单项在可用空间不足时不换行,宽度取最长条目;
  • set_additional_classes(string $class):给菜单容器追加样式类;
  • set_owner_selector($selector):设置data-owner,用于指定菜单的所属节点;
  • prioritise公开属性:为true时触发器会被渲染在主操作之前(对应 action_menu.mustache 中的{{#prioritise}}分支)。

菜单项类型详解

add()方法接受多种条目类型,下面逐一说明。

action_menu_link

action_menu_link(源码类名 link)是链接条目的通用类,构造参数如下:

参数类型说明
$urlmoodle_url链接地址
$iconpix_icon(可空)可选的图标;若不传,条目会显示触发器图标;kebab 菜单下则可能不显示任何图标
$textstring要显示的文本
$primarybool条目是主操作还是次级操作,默认值为true(主操作)
$attributesarray可选的 HTML 属性数组

源码构造函数签名为__construct(moodle_url $url, ?pix_icon $icon, $text, $primary = true, array $attributes = []),与文档描述一致。

两个便捷子类直接继承action_menu_link:

  • action_menu_link_primary(源码 link_primary.php):固定以$primary = true构造,作为主操作加入;
  • action_menu_link_secondary(源码 link_secondary.php):固定以$primary = false构造,作为次级操作加入。

这两个类都通过class_alias保持了旧类名的兼容,因此新旧代码都可以正常使用。

pix_icon

action_menu可以把pix_icon直接作为主操作渲染。pix_icon是 Moodle 生成图标的标准输出类(见 pix_icon.php),构造参数如下:

参数类型说明
$pixString图标内部位置,例如"t/user"、"t/edit"、"i/menu"
$altString可选的替代文本;传空串可让图标变为装饰性图标(自动加aria-hidden)
$componentString图标所属组件。源码默认值为'moodle'(即核心pix文件夹中的图标);插件图标需传入对应组件名,例如'mod_forum'
$attributesArray可选的 HTML 属性

注意源码默认的$component是'moodle',而文档所说“默认仅使用核心 pix 文件夹中的图标”正对应这一默认值。此外,若图标名称以b/开头(大图标),类会自动追加iconsize-big样式类。

core\output\local\action_menu\subpanel

subpanel让动作菜单能够添加“悬停或点击时展开子面板”的条目。子面板内容可以是任意 renderable 对象(例如choicelist),渲染时会使用标准output::render方法。

构造参数:

参数类型说明
$textstring菜单项中显示的文本
$subpanelrenderable要在子面板内部渲染的输出对象
$attributesarray可选的 HTML 属性

源码还额外支持两个可选参数:?pix_icon $icon = null(条目图标)与?moodle_url $url = null(条目 URL,缺省时条目不可点击、仅用于展开子面板),并会自动为条目生成随机id(见 subpanel.php 构造实现)。export_for_template()中还有一个细节:为了避免子面板条目的图标与菜单触发器图标冲突,模板上下文使用独立的itemicon变量(unset($data->icon))。

下面的例子用 renderable 的choicelist实例创建子面板:

/** @var core_renderer $output*/ $output = $PAGE->get_renderer('core'); // A choice list is a renderable class to outpout a user choice. $choice = new core\output\choicelist('Choice example'); $choice->add_option("statusa", "Status A", [ 'url' => $PAGE->url, 'description' => 'Status A description', 'icon' => new pix_icon('t/user', '', ''), ]); $choice->add_option("statusb", "Status B", [ 'url' => $PAGE->url, 'description' => 'Status B description', 'icon' => new pix_icon('t/groupv', '', ''), ]); $choice->set_selected_value('statusb'); $menu = new action_menu(); // Add subpanel item. $menu->add(new core\output\local\action_menu\subpanel( 'Subpanel example', $choice )); echo $output->render($menu);

在模板层面,subpanel.mustache 渲染为一个dropdown-subpanel position-relative dropend容器,内部是带data-toggle="dropdown-subpanel"的菜单项链接和dropdown-subpanel-content内容区,同时通过require(['core/local/action_menu/subpanel'], ...)加载 AMD 模块完成展开交互。Behat 行为测试 action_menu_subpanel.feature 验证了鼠标与键盘两种导航方式:点击菜单项打开子面板、使用方向键在子面板内移动焦点,以及点击子面板中的链接后页面正确接收参数(测试页面 action_menu_subpanel_output_testpage.php 通过 URL 参数foo回显选择结果)。测试还覆盖了向左展开(menu left)、条目带图标、额外 data 属性透传等场景。

HTML 字符串

如果向action_menu添加一个普通字符串,add()会把它原样打印在下拉菜单内部(作为次级操作)。这在需要插入自定义 HTML、分隔线或说明文字时非常实用,源码分发逻辑中的else分支正是$this->add_secondary_action($action)。

组件库示例与可运行演示

Moodle 组件库(Component Library)为动作菜单提供了独立的可运行示例页 examples/actionmenu.php,原文档正是通过 iframe 将这一示例嵌入组件库文档页。该示例依次演示了:

  • 默认动作菜单:未做任何定制,触发器使用默认图标与 caret;
  • Kebab 菜单:set_kebab_trigger(get_string('edit'), $output)+set_additional_classes('fields-actions');
  • 自定义文本触发器:set_menu_trigger(get_string('edit'));
  • 仅图标触发器:pix_icon('i/moremenu', '')+set_action_label(get_string('moremenu')),并附title属性;
  • 移除 caret:triggerextraclasses = 'no-caret';
  • 图标 + 可见文本 / 图标 + 视觉隐藏文本:两种可访问触发器组合;
  • 带主操作的菜单:追加多个action_menu_link_primary条目。

该示例还展示了子面板与choicelist的组合用法(子面板内容为带图标与描述的状态选择列表)。如果你在本仓库环境中打开组件库,可以在相应文档页直接交互体验这几种形态。

无障碍与最佳实践小结

结合原文档与源码,使用 Action Menu 时建议遵守以下要点:

  1. 先定位置再添加:条目是主操作还是次级操作取决于传入add()时的类型与primary标志,且必须在添加前确定;
  2. 图标触发器必须提供可访问名称:要么用set_action_label(),要么在图标旁追加visually-hidden文本;同时把pix_icon的$alt传空,让其成为装饰性图标(源码会自动添加aria-hidden="true");
  3. 善用 ARIA 默认行为:主/次操作条目的role="menuitem"、触发器按钮的aria-haspopup、aria-expanded、aria-controls均由组件自动生成,无需手工编写;
  4. 子面板内容必须是 renderable:subpanel构造时直接传入对象,渲染期通过标准render()输出,因此任何输出类(choicelist、自定义面板等)都可以作为子面板内容;
  5. 注意菜单空间:示例页明确提示动作菜单不适合在 iframe 中展示;在窄容器中可考虑set_menu_left()、set_boundary('viewport')或set_nowrap_on_items()来控制布局。

掌握了这些 API 与源码实现细节后,你可以在自己的 Moodle 插件或主题中快速构建风格统一、键盘友好、可访问的动作菜单。

  • 教育
  • 后端
  • 前端

【免费下载链接】moodle

Moodle - the world's open source learning platform

项目地址:https://gitcode.com/gh_mirrors/mo/moodle
点击查看免费下载
上一篇:VirtualApp深度剖析:重新定义Android应用虚拟化的终极指南
下一篇:VRCX完整指南:解锁VRChat社交管理的终极利器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询