111. 菜单上下文连接Menu contextual links

drupal可以为页面中的局部区域提供额外的链接,通常用这些链接指向和这个区域相关的页面,这些链接就是本篇所说的菜单上下文链接,在drupal中被大量运用,她们位于哪里呢?以默认安装为例:以管理员身份登录系统后,打开首页,当鼠标悬停在某个块上时,该块右上方将出现一个图标,默认样式为一个圆圈,里面有一只铅笔,该图标就是菜单上下文链接按钮图标,后简称上下文图标或上下文链接按钮,点击上下文图标后将出现一个链接列表,这些链接就是菜单上下文链接,点击链接将跳转页面或执行某功能;页面中可以有许多区域都拥有上下文链接,每个区域的链接都由区域自定义,当鼠标在该区域停留时,如果该区域有上下文链接,就会在其右上方显示上下文图标,鼠标移出后切换为隐藏,如何将整个页面的上下文图标都显示出来呢?在工具栏(顶部黑色条)的右侧有一个编辑按钮(其左侧有一个铅笔图标),点击编辑按钮后页面中所有上下文图标都同时显示出来了,再次点击或按esc键将消失,这样反复点击将切换显示状态,如果页面中一个上下文图标也没有,工具栏将不会显示编辑按钮(见下文)。

菜单上下文链接功能可能是你之前没有留意到的,在继续阅读本篇前,可以先体验一下,以便更好的理解本篇后续内容;通常每一个块渲染区都会有上下文图标,包含着“配置区块”链接,有些块还会有其他上下文链接,比如菜单块还有“编辑菜单”链接,默认首页中节点列表的每个节点都有上下文图标,点击后有“快速编辑、编辑、删除、翻译”等上下文链接,有的跳转到相关页面,有的就地执行一些功能,这些上下文链接极大提升了使用体验,缩短了操作时间,不得不说菜单上下文链接是系统提供的一个非常强大、好用的功能。

 

上下文链接显示条件:

需要具备以下三个条件才会显示上下文图标:

1、浏览器有js支持,菜单上下文功能的实现是后台程序和前台js共同完成的,还依赖ajax

2、用户有访问上下文链接的权限,权限标识:‘access contextual links’,权限label:“使用上下文链接”,这也就是前文要求管理员登录的原因,实际上只要拥有该权限的账户即可,默认匿名用户是没有的。

3、页面区域有上下文链接定义

以上三点中前两点较好理解,下文讲解如何定义页面某区域的上下文链接。

 

上下文链接插件定义:

上下文定义分两个部分:插件定义和渲染数组定义,首先系统中存在的每一个上下文链接,系统均以插件方式定义,一个上下文链接对应一个插件,插件由菜单上下文链接插件管理器收集并管理(见下),插件定义在模块根目录的以下文件中:

  “模块机器名.links.contextual.yml

其中根键作为插件id,其值作为插件定义,有以下键名:

route_name:上下文链接所指页面的路由名,换句话说即点击后跳转到该路由,字符串值,必填项

title:上下文链接文本,字符串值,被系统理解为可翻译的,在系统内部被转化为翻译对象TranslatableMarkup,用于链接显示的文本,并非title属性值

title_context:可选,标题翻译上下文,见翻译系统

group:本上下文链接所属的组,必填,在渲染数组中以组为单位使用这些插件

weight:排序权重,默认为null

options:链接选项,见本系列URL篇,可在其中指定链接属性,如是否在新窗口打开、添加类名等

id:插件id,无需指定,系统以yml文件中的定义根键自动赋值,根键最佳实践推荐采用路由名,如果多个定义有相同路由名,那么可以采用后缀加以区分

provider:提供插件定义的模块名,由系统赋值

deriver:插件派生器的全限定类名,见插件系统

class:实例化插件对象的默认实现类,默认为“\Drupal\Core\Menu\ContextualLinkDefault”,当有特殊需要时可以自定义,在默认安装中全部采用了该默认类

注意:以上定义中没有路由参数route_parameters定义,那在渲染数组中传递,见后。

 

应用上下文链接的区域渲染数组定义:

当上下文链接插件定义好后,她们只是存在了,还没有被使用,具体被用到哪个页面区域呢?这就要看区域的渲染数组定义了,这里把产生要应用上下文链接的区域的渲染数组称为区域渲染数组,区域渲染数组要运用上下文链接要满足几个条件:

首选需要采用主题钩子,且对应的模板有要求(见下)。

其次需要定义'#contextual_links'属性,该属性的值称为上下文定义数组,如下所示:

'#contextual_links' => [
                ' groupID_1' => ['route_parameters' => [],'metadata' => []],
                ' groupID_2' => ['route_parameters' => [],'metadata' => []],
            ],

其中groupID就是插件定义中的group值,上下文链接定义可以包含多个组,属于这些组的上下文链接(插件定义)都会被应用到该区域,参数route_parameters的值是插件定义中路由的参数,参数metadata是可选值,为功能扩展而存在,可用来传递特定功能所需的额外数据,下文相关钩子可以使用它进行额外判断等,属性'#contextual_links'在区域渲染数组中的位置依据其所用主题钩子接收变量的方式分两种情况:

1、如果主题钩子接受整个渲染数组传入($info['render element']方式注册钩子),那么上下文定义数组放置在区域渲染数组最外层,换句话说属性#contextual_links是区域渲染数组的一级键名

2、如果主题钩子仅接受部分变量($info['variables']方式),那么上下文定义数组应该放置在第一个变量下,“第一个”是指主题钩子注册时第一个声明的变量名,即$info['variables']中的第一个变量,并非指区域渲染数组中第一个属性,此时键名#contextual_links是区域渲染数组的第二级键名

更多信息请参考本系列主题钩子注册篇

 

示例说明:

这里假设主题钩子为"yunke_contextual_links",组idyunke_node_add

如果是第一种情况($info['render element']方式),区域渲染数组$contextual类似这样:

        $contextual = [
            '#theme'            => "yunke_contextual_links",
            '#contextual_links' => [
                'yunke_node_add' => ['route_parameters' => [],'metadata' => []],
            ],
        ];

如果是第二种情况($info['variables']方式),假设第一个变量名为yunke,区域渲染数组$contextual类似这样:

        $contextual = [
            '#theme' => "yunke_contextual_links",
            '#node'  => $node,
            '#yunke' => ['#contextual_links' => [
                'yunke_node_add' => ['route_parameters' => [], 'metadata' => []],
                ],
            ],
        ];

 

模板定义:

有了插件定义、区域渲染数组定义后还不够,要将上下文链接显示出来,模板对应的主题钩子定义必须具备以下通用预处理函数:

   function contextual_preprocess(&$variables, $hook, $info)

默认情况下系统为所有模块注册的主题钩子都附加了该预处理函数,她用来处理上下文链接,提供模板中的特定变量,位于以下文件中:

   core/modules/contextual/contextual.module

此外模板中必须要打印以下变量:

   {{ title_suffix.contextual_links }}

打印处的外层元素需要添加{{attributes}}属性,目的是给包装元素添加类名:"contextual-region",该类名用于定位上下文图标按钮出现的位置

注意:官网文档提到要显示上下文链接的模板需要打印:{{ title_suffix }},这是不准确的,因为title_suffix不仅仅用于上下文链接,还包含有其他内容

 

菜单上下文链接应用示例:

为了读者有更加直观的理解,我们来实践一下上下文链接的应用,这里假设模块名为“yunke:

先在模块根目录建立文件“yunke.links.contextual.yml”,内容如下:

yunke.node.add_page:
  title: 'yunke node add'
  group: yunke_node_add
  route_name: 'node.add_page'

这定义了一个上下文链接插件,链接指向节点添加页(路径:/node/add);然后注册一个主题钩子,在文件yunke.module中添加以下函数:

function yunke_theme()
{
    return [
        'yunke_contextual_links' => [
            'render element' => 'yunke',
        ],
    ];
}

接着建立这个主题钩子的模板文件,在模块根目录的templates文件夹下建立以下文件:

   yunke-contextual-links.html.twig

内容为:

<div {{attributes}}>
  云客模块上下文链接应用测试,鼠标移动到这里将出现上下文图标:
  {{ title_suffix.contextual_links }}
</div>

然后建立路由和控制器,在控制器中运行以下代码:

        $contextual = [
            '#theme'            => "yunke_contextual_links",
            '#contextual_links' => [
                'yunke_node_add' => ['route_parameters' => [], 'metadata' => []],
            ],
        ];
        return $contextual;

最后访问这个控制器,将鼠标移动到渲染输出的地方,你将看到上下文图标显示,点击将显示'yunke node add'连接,点击该连接后页面将跳转到“/node/add”页面

 

以上就是菜单上下文链接在使用层面的所有内容了,接下来我们讲解系统是如何实现她的。

 

菜单上下文链接插件管理器

首先是菜单上下文连接(Menu contextual links)插件管理器,用于收集管理所有的插件,定义如下:

服务idplugin.manager.menu.contextual_link

类:Drupal\Core\Menu\ContextualLinkManager

插件修改钩子:contextual_links_plugins

 

主要方法解释如下:

public function getContextualLinkPluginsByGroup($group_name)

根据组名查找上下文插件定义,返回一个数组,键名为插件ID,键值为插件定义

 

public function getContextualLinksArrayByGroup($group_name, array $route_parameters, array $metadata = [])

按组返回一个数组,用于将上下文插件转化为链接模板所需的参数形式,键名为插件id,键值如下:

[
        'route_name' => $route_name,
        'route_parameters' => $route_parameters,
        'title' => $plugin->getTitle($request),
        'weight' => $plugin->getWeight(),
        'localized_options' => $plugin->getOptions(),
        'metadata' => $metadata,
      ];

用户无权访问的上下文链接被过滤,该方法派发修改钩子:contextual_links,函数签名为:

   function hook_contextual_links_alter(array &$links, $group, array $route_parameters)

参数$links就是该方法的返回数组(在返回前执行钩子),键名为插件id,键值如上。

 

上下文链接的实现:

上下文链接并不是在一次请求中完成渲染的,她借助了AJAX,过程如下:

在渲染区域渲染数组时,首先执行的是上下文预处理函数:

  core/modules/contextual/contextual.module::contextual_preprocess(&$variables, $hook, $info)

在该预处理函数中将为模板准备将用到的两个变量:

   {{ title_suffix.contextual_links }}{{attributes}}

其中{{ title_suffix.contextual_links }}实际上是如下的渲染数组(称为:上下文占位渲染数组):

    [
      '#type' => 'contextual_links_placeholder',
      '#id' => $contextual_links _id,
    ];

这里#id是上下文链接id,是一个特定格式的字符串值,包含着上下文定义数组的全部信息(组、路由参数、额外信息),可通过该id完整还原上下文定义数组,元素类型'contextual_links_placeholder'主要作用是产生一个验证token,见:\Drupal\contextual\Element\ContextualLinksPlaceholder

上下文占位渲染数组最终将渲染出类似如下的内容:

<div data-contextual-id="yunke_node_add::langcode=zh-hans" data-contextual-token="pgg-0cWmA6GRAuGtW5f1WGo1Yx726xRCNfKhW_jjTis"></div>

这称为上下文链接占位元素,至此第一次请求的工作就结束了,接下来由前端js依据上下文占位元素向服务器发起AJAX,取回上下文链接实际元素后放置到占位元素中,做此工作的js文件如下:

   core/modules/contextual/js/contextual.js

js资源是通过hook_page_attachments()钩子添加到页面中的,见:

   core/modules/contextual/contextual.module::contextual_page_attachments(array &$page)

她通过ajaxpost方式向服务器发起的第二次请求传递页面中所有上下文占位元素的“data-contextual-id”和“data-contextual-token”属性值,服务器端地址为:'/contextual/render',控制器如下:

   \Drupal\contextual\ContextualController::render

在该控制器中将渲染真正的上下文链接列表并以json方式返回,这将由以下类型的渲染数组负责:

      [
        '#type' => 'contextual_links',
        '#contextual_links' => _contextual_id_to_links($id),
      ];

这里上下文定义数组通过id值被还原了,元素类型'contextual_links'的类名如下:

   \Drupal\contextual\Element\ContextualLinks

该类为区域渲染数组构建最终使用的上下文链接渲染数组,并派发以下修改钩子:

   'contextual_links_view'

钩子函数签名如下:

   function hook_contextual_links_view_alter(&$element, $items)

参数$element为上下文链接渲染数组(见contextual_links 元素类型的getInfo()方法),参数$items是上下文链接插件管理器的getContextualLinksArrayByGroup方法返回的数组(多个组合并的结果),这里列举一个该修改钩子的用途:假设我们需要为每一个上下文链接添加一个类名:“yunkeClass”,模块名为yunke,如下:

function yunke_contextual_links_view_alter(&$element, $items)
{
    if (empty($element['#links'])) {
        return;
    }
    $class = 'yunkeClass';
    foreach ($element['#links'] as &$link) {
        $options = $link['url']->getOptions();
        if (isset($options['attributes']['class'])) {
            if (is_array($options['attributes']['class'])) {
                $options['attributes']['class'][] = $class;
            } else {
                $options['attributes']['class'] = [$options['attributes']['class'], $class];
            }
        } else {
            $options['attributes']['class'] = [$class];
        }
        $link['url']->setOptions($options);
    }
}

 

工具栏编辑按钮:

工具栏中的上下文“编辑”按钮是通过以下钩子添加的(详见本系列工具栏主题):

   core/modules/contextual/contextual.module::contextual_toolbar()

页面加载完成后初始时该按钮处于隐藏状态,仅在页面中至少有一个上下文按钮时才显示出来(由js侦查是否存在上下文占位元素,有属性data-contextual-id的元素),如果页面中一个上下文按钮也没有(没有上下文占位元素),则工具栏中的编辑按钮将一直保持隐藏状态,这也是有些页面没有该按钮的原因,比如内容管理页(/admin/content

 

非主题钩子方式实现上下文链接:

前文已经讲到默认情况下,实现上下文链接需要采用主题钩子方式,且对模板也有要求,如果不用主题钩子能实现吗?答案是能,但这种用法是罕见的(drupal十分灵活、强大,很多事几乎总能实现),在明白以上原理后以非主题方式实现上下文链接只需做到两点即可:

1、在页面中产生一个上下文占位元素

2、在该上下文占位元素的外层包装元素上添加一个类名:'contextual-region'以定位上下文图标的位置

 

这里提供一个演示,在控制器中执行以下代码:

        $contextual_definition = [
            'block' => ['route_parameters' => ['block' => 'bartik_main_menu'],
                        'metadata'         => []],
        ];
        //这里$contextual_definition等同于#contextual_links
        $contextual_links = ['#id' => _contextual_links_to_id($contextual_definition)];
        $elementTypeClass = '\Drupal\contextual\Element\ContextualLinksPlaceholder';
        $contextual_links = $elementTypeClass::preRenderPlaceholder($contextual_links);
        //构建上下文链接占位元素
        $contextual = [
            '#type'       => 'container',
            '#attributes' => ['class' => ['contextual-region']],
            'placeholder' => $contextual_links,
            'element'     => ['#markup' => '区域渲染数组的正常内容'],
        ];
        return $contextual;

 

该示例实现了一个上下文链接(点击后跳转到块配置页面),但并没有使用主题钩子,核心在于放置了上下文占位元素,并设置了容器元素,在其上添加了用于定位上下文图标显示的类,区域数组中须显示的其他内容作为$contextual的子元素即可

 

补充:

1、上下文链接模块的本地帮助地址:/admin/help/contextual,官网文档:

https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Menu%21m…

2、上下文链接功能主要用作管理目的,链接是由路由定义的,因此只能指向内部地址。

3、如果在上下文插件定义中,同一个组内有不同的路由名,区域渲染数组中上下文定义时,需要为该组内所有路由传递所有必须的路由参数,如果组内不同路由间有同名路由参数,但参数含义不一样,换句话说参数名相同,但值应该不同,这种情况称为参数冲突,存在参数冲突的路由不应该被定义在同一个组内,这种情况是很罕见的。

 

 

本书共115小节:

评论 (写第一个评论)