Restrict Panels Content Per Region

The Panels module is a great layout tool for Drupal site builders and content creators. On a recent build, I needed to be able to restrict the types of content that end users could insert into specific regions. For example, users should only be able to insert Buttons and Quotes in the Second Sidebar region and Header Images in the Header region.

Before (with a plethora of options sufficient to overwhelm users):

Panels Add Content Modal beforehand

After (with only white-listed options):

Panels Add Content Modal afterward

Solution = Override Panels Template Function

The solution was to use moduleName_preprocess_hook() to override template_preprocess_panels_add_content_modal(), which is provided by Panels. If a type of content isn't allowed, then it gets unset, and thus isn't available to the user. The override function is below:

/*
* Implements moduleName_preprocess_hook().
*
* Overrides template_preprocess_panels_add_content_modal().
* Provide options to add only allowed content in Panels Add Content modal.
*/

function MYMODULE_preprocess_panels_add_content_modal(&$vars) {

  $layout = $vars['renderer']->display->layout;
  $region = $vars['region'];

  // Iterate through each category.
  foreach($vars['categories'] as $key_category => $category) {
    // Iterate through each possible content item.
    foreach($category['content'] as $key_content => $content) {
      // Check if content item is allowed in current region (in current layout).
      $allowed = MYMODULE_allowed_content($content, $layout, $region);

      // If content item is not allowed, then unset it.
      if($allowed == FALSE) {
        unset($vars['categories'][$key_category]['content'][$key_content]);
      }
    }
    // Check for empty categories and unset if empty.
    if(empty($vars['categories'][$key_category]['content'])) {
      unset($vars['categories'][$key_category]);
    }
  }

  // All code below is copied from template_preprocess_panels_add_content_modal(), which
  // this function is overriding.
  $vars['categories_array'] = array();

  // Process the list of categories.
  foreach ($vars['categories'] as $key => $category_info) {
    // 'root' category is actually displayed under the categories, so
    // skip it.
    if ($key == 'root') {
      continue;
    }

    $class = 'panels-modal-add-category';
    if ($key == $vars['category']) {
      $class .= ' active';
    }

    $url = $vars['renderer']->get_url('select-content', $vars['region'], $key);
    $vars['categories_array'][] = ctools_ajax_text_button($category_info['title'], $url, '', $class);
  }

  // Now render the top level buttons (aka the root category) if any.
  $vars['root_content'] = '';
  if (!empty($vars['categories']['root'])) {
    foreach ($vars['categories']['root']['content'] as $content_type) {
      $vars['root_content'] .= theme('panels_add_content_link', array('renderer' => $vars['renderer'], 'region' => $vars['region'], 'content_type' => $content_type));
    }
  }
}

I also wrote a helper function to determine if a particular type of content is allowed. Allowed content is defined in an array. (The allowed-content definition could be exposed to site builders through an admin UI; however, this wasn't necessary in my use case.) The helper function is below:

/*
* Helper function to determine if a type of content is allowed in
* a region of a given layout.
*
* Configuration for allowed content is stored in the $allowed_content array:
*
* $allowed content = array(
*   LAYOUT => array(
*     REGION => array(
*       SUBTYPE_NAME => TYPE_NAME,
*       SUBTYPE_NAME => TYPE_NAME
*     ),
*     REGION => array(
*       ...
*     )
*   ),
*    LAYOUT => array(
*       ...
*    )
*  );
*
*  SUBTYPE_NAME => TYPE_NAME are reversed because array keys need to be unique.
*
* @param array $content
*   Content being evaluated.
* @param string $layout
*   Layout machine name of the current panel.
* @param string $region
*   Region machine name to which content is being added.
* @return bool $return
*   TRUE if content is allowed; FALSE if content is not allowed.
*/

function MYMODULE_allowed_content($content,$layout, $region) {

  //Define allowed content.
  $allowed_content = array(
    'myproject_panel_layout' => array(
      'header' => array(
        'myproject_page_title' => 'myproject_page_title',
        'header_image' => 'fieldable_panels_pane'
      ),
      'content' => array(
        'accordion' => 'fieldable_panels_pane',
        'content' => 'fieldable_panels_pane'
      ),
      'sidebar_1' => array(
        'myproject_sidebar_nav' => 'myproject_sidebar_nav',
        'quick_links' => 'fieldable_panels_pane'
      ),
      'sidebar_2' => array(
        'button' => 'fieldable_panels_pane',
        'quote' => 'fieldable_panels_pane'
      ),
      'subfooter' => array(
        'sub_footer_content' => 'fieldable_panels_pane'
      ),
    ),
  );

  // If current layout isn't controlled, then all content is allowed.
  if(!array_key_exists($layout,$allowed_content)) {
    return TRUE;
  }
 
  $type_name = $content['type_name'];
  $subtype_name = $content['subtype_name'];
 
  // If subtype_name is fpid:% or vid:%, then substitute Fieldable Panels Panes bundle.
  if(substr($content['subtype_name'], 0, 4 ) === "fpid") {
    $subtype_name = $content['bundle'];
  }
  elseif(substr($content['subtype_name'], 0, 3 ) === "vid") {
    $subtype_name = $content['bundle'];
  }

  if(isset($allowed_content[$layout][$region][$subtype_name])
    && $allowed_content[$layout][$region][$subtype_name] == $type_name) {
      $return = TRUE;
    }
  else{
    $return = FALSE;
  }
  return $return;
}

Fieldable Panels Panes Considerations (and Patch)

In this case, I was using the Fieldable Panels Panes (FPP) module and needed to determine the bundle (e.g. 'button', 'quote') from the subtype:

  // If subtype_name is fpid:% or vid:%, then substitute Fieldable Panels Panes bundle.
  if(substr($content['subtype_name'], 0, 4 ) === "fpid") {
    $subtype_name = $content['bundle'];
  }
  elseif(substr($content['subtype_name'], 0, 3 ) === "vid") {
    $subtype_name = $content['bundle'];
  }

For the FPP-related code above to work, you'll need to apply the following patch: fieldable_panels_panes-include-bundle-in-subtype-2637740-11.patch (Include "bundle" property with FPP entity content types)


Note: If you're like the Panels module, you should also check out the Panelizer module, along with the Fieldable Panels Panes module.

Add new comment