雲客Drupal源碼分析之菜單上下文連接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",組id爲yunke_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)插件管理器,用於收集管理所有的插件,定義如下:

服務id:plugin.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)

她通過ajax以post方式向服務器發起的第二次請求傳遞頁面中所有上下文佔位元素的“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%21menu.api.php/group/menu

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

3、如果在上下文插件定義中,同一個組內有不同的路由名,區域渲染數組中上下文定義時,需要爲該組內所有路由傳遞所有必須的路由參數,如果組內不同路由間有同名路由參數,但參數含義不一樣,換句話說參數名相同,但值應該不同,這種情況稱爲參數衝突,存在參數衝突的路由不應該被定義在同一個組內,這種情況是很罕見的。

 

我是雲客,【雲遊天下,做客四方】,聯繫方式見主頁,歡迎轉載,但須註明出處

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章