Adaptar a Bootstrap el layout builder del módulo Panels

  • 5 Oct 2015
  • Drupal 7

Panels es un antes y un después en Drupal. Es como la continuación de un cuento mágico que empieza a contar Views. Panels, mini panes y pages le permiten al maestro de obra que lleva adelante una construcción con Drupal llegar a casa más relajado y menos sucio. Pero cuál es el mejor modo de llevar a cabo tal desarrollo? Cuál es la práctica más favorable para crear una estructura fuerte pero al mismo tiempo fácil de mantener? Hoy no vamos a dar ninguna respuesta a estas preguntas. De lo que sí vamos a hablar es de como utilizar el layout "builder" con Bootstrap.

El layout builder (Lego like)

Cuando se crea un nuevo panel o un mini panel, llega un momento en que se solicita al maestro de obra la selección de un layout. El layout nos permitirá más adelante organizar el o los contenidos. El layout builder a diferencia del resto nos permite agregar elementos (como columnas, filas y regiones) para construir así un layout a la medida de nuestras necesidades.

Lamentablemente, lo que este layout llama canvas, column, row, region no tienen nada que ver los homólogos elementos definidos en el framework Bootstrap. Nada que ver. Entonces? Cómo hacemos que builder funcione perfectamente en nuestro tema desarrollado a partir de Bootstrap?

El capitán garfio al rescate

Antes de zambullirnos en código php, volvemos a las áreas definidas por el layout builder: column, row y region. Para adaptar el layout a un theme de Bootstrap vamos a traducirlas a: container, row, column

Si miramos el código del layout builder (que se encuentra en panel/plugins/layouts/flexible/flexible.inc), encontraremos lo siguiente:

/**
 * Draw the flexible layout.
 */
function theme_panels_flexible($vars) {
  $css_id = $vars['css_id'];
  $content = $vars['content'];
  $settings = $vars['settings'];
  $display = $vars['display'];
  $layout = $vars['layout'];
  $handler = $vars['renderer'];

  panels_flexible_convert_settings($settings, $layout);

  $renderer = panels_flexible_create_renderer(FALSE, $css_id, $content, $settings, $display, $layout, $handler);

  // CSS must be generated because it reports back left/middle/right
  // positions.
  $css = panels_flexible_render_css($renderer);

  if (!empty($renderer->css_cache_name) && empty($display->editing_layout)) {
    ctools_include('css');
    // Generate an id based upon rows + columns:
    $filename = ctools_css_retrieve($renderer->css_cache_name);
    if (!$filename) {
      $filename = ctools_css_store($renderer->css_cache_name, $css, FALSE);
    }

    // Give the CSS to the renderer to put where it wants.
    if ($handler) {
      $handler->add_css($filename, 'module', 'all', FALSE);
    }
    else {
      drupal_add_css($filename);
    }
  }
  else {
    // If the id is 'new' we can't reliably cache the CSS in the filesystem
    // because the display does not truly exist, so we'll stick it in the
    // head tag. We also do this if we've been told we're in the layout
    // editor so that it always gets fresh CSS.
    drupal_add_css($css, array('type' => 'inline', 'preprocess' => FALSE));
  }

  // Also store the CSS on the display in case the live preview or something
  // needs it
  $display->add_css = $css;

  $output = "<div class=\"panel-flexible " . $renderer->base['canvas'] . " clearfix\" $renderer->id_str>\n";
  $output .= "<div class=\"panel-flexible-inside " . $renderer->base['canvas'] . "-inside\">\n";

  $output .= panels_flexible_render_items($renderer, $settings['items']['canvas']['children'], $renderer->base['canvas']);

  // Wrap the whole thing up nice and snug
  $output .= "</div>\n</div>\n";

  return $output;
}
/**
 * Render a piece of a flexible layout.
 */
function panels_flexible_render_items($renderer, $list, $owner_id) {
  $output = '';
  $groups = array('left' => '', 'middle' => '', 'right' => '');
  $max = count($list) - 1;
  $prev = NULL;

  foreach ($list as $position => $id) {
    $item = $renderer->settings['items'][$id];
    $location = isset($renderer->positions[$id]) ? $renderer->positions[$id] : 'middle';

    if ($renderer->admin && $item['type'] != 'row' && $prev ) {
      $groups[$location] .= panels_flexible_render_splitter($renderer, $prev, $id);
    }

    switch ($item['type']) {
      case 'column':
        $content = panels_flexible_render_items($renderer, $item['children'], $renderer->base['column'] . '-' . $id);
        if (empty($renderer->settings['items'][$id]['hide_empty']) || trim($content)) {
          $groups[$location] .= panels_flexible_render_item($renderer, $item, $content, $id, $position, $max);
        }
        break;
      case 'row':
        $content = panels_flexible_render_items($renderer, $item['children'], $renderer->base['row'] . '-' . $id);
        if (empty($renderer->settings['items'][$id]['hide_empty']) || trim($content)) {
          $groups[$location] .= panels_flexible_render_item($renderer, $item, $content, $id, $position, $max, TRUE);
        }
        break;
      case 'region':
        if (empty($renderer->settings['items'][$id]['hide_empty'])) {
          $content = isset($renderer->content[$id]) ? $renderer->content[$id] : "&nbsp;";
        }
        else {
          $content = isset($renderer->content[$id]) ? trim($renderer->content[$id]) : "";
        }
        if (empty($renderer->settings['items'][$id]['hide_empty']) || $content) {
          $groups[$location] .= panels_flexible_render_item($renderer, $item, $content, $id, $position, $max);
        }
        break;
    }

    // If all items are fixed then we have a special splitter on the right to
    // control the overall width.
    if (!empty($renderer->admin) && $max == $position && $location == 'left') {
      $groups[$location] .= panels_flexible_render_splitter($renderer, $id, NULL);
    }
    $prev = $id;
  }

  $group_count = count(array_filter($groups));

  // Render each group. We only render the group div if we're in admin mode
  // or if there are multiple groups.
  foreach ($groups as $position => $content) {
    if (!empty($content) || $renderer->admin) {
      if ($group_count > 1 || $renderer->admin) {
        $output .= '<div class="' . $owner_id . '-' . $position . '">' . $content . '</div>';
      }
      else {
        $output .= $content;
      }
    }
  }

  return $output;
}

/**
 * Render a column in the flexible layout.
 */
function panels_flexible_render_item($renderer, $item, $content, $id, $position, $max, $clear = FALSE) {

  // If we are rendering a row and there is just one row, we don't need to
  // render the row unless there is fixed_width content inside it.
  if (empty($renderer->admin) && $item['type'] == 'row' && $max == 0) {
    $fixed = FALSE;
    foreach ($item['children'] as $id) {
      if ($renderer->settings['items'][$id]['width_type'] != '%') {
        $fixed = TRUE;
        break;
      }
    }

    if (!$fixed) {
      return $content;
    }
  }

  // If we are rendering a column and there is just one column, we don't
  // need to render the column unless it has a fixed_width.
  if (empty($renderer->admin) && $item['type'] == 'column' && $max == 0 && $item['width_type'] == '%') {
    return $content;
  }

  $base = $renderer->item_class[$item['type']];
  $output = '<div class="' . $base . ' ' . $renderer->base[$item['type']] . '-' . $id;
  if ($position == 0) {
    $output .= ' ' . $base . '-first';
  }
  if ($position == $max) {
    $output .= ' ' . $base . '-last';
  }
  if ($clear) {
    $output .= ' clearfix';
  }

  if (isset($item['class'])) {
    $output .= ' ' . check_plain($item['class']);
  }

  $output .= '">' . "\n";

  if (!empty($renderer->admin)) {
    $output .= panels_flexible_render_item_links($renderer, $id, $item);
  }

  $output .= '  <div class="inside ' . $base . '-inside ' . $base . '-' . $renderer->base_class . '-' . $id . '-inside';
  if ($position == 0) {
    $output .= ' ' . $base . '-inside-first';
  }
  if ($position == $max) {
    $output .= ' ' . $base . '-inside-last';
  }
  if ($clear) {
    $output .= ' clearfix';
  }

  $output .= "\">\n";
  $output .= $content;
  $output .= '  </div>' . "\n";
  $output .= '</div>' . "\n";

  return $output;
}

Aquí tenemos las tres funciones mágicas: theme_panels_flexible, panels_flexible_render_item y panels_flexible_render_items.

Estas funciones son las encargadas de generar el html de nuestro panel. Podemos redefinir en nuestro tema la función mitema_panels_flexible.

function mitema_panels_flexible($vars) {
  $css_id = $vars['css_id'];
  $content = $vars['content'];
  $settings = $vars['settings'];
  $display = $vars['display'];
  $layout = $vars['layout'];
  $handler = $vars['renderer'];

  panels_flexible_convert_settings($settings, $layout);

  $renderer = panels_flexible_create_renderer(FALSE, $css_id, $content, $settings, $display, $layout, $handler);

  // CSS must be generated because it reports back left/middle/right
  // positions.
  $css = panels_flexible_render_css($renderer);

  if (!empty($renderer->css_cache_name) && empty($display->editing_layout)) {
    ctools_include('css');
    // Generate an id based upon rows + columns:
    $filename = ctools_css_retrieve($renderer->css_cache_name);
    if (!$filename) {
      $filename = ctools_css_store($renderer->css_cache_name, $css, FALSE);
    }

    // Give the CSS to the renderer to put where it wants.
    if ($handler) {
      $handler->add_css($filename, 'module', 'all', FALSE);
    }
    else {
      drupal_add_css($filename);
    }
  }
  else {
    // If the id is 'new' we can't reliably cache the CSS in the filesystem
    // because the display does not truly exist, so we'll stick it in the
    // head tag. We also do this if we've been told we're in the layout
    // editor so that it always gets fresh CSS.
    drupal_add_css($css, array('type' => 'inline', 'preprocess' => FALSE));
  }

  // Also store the CSS on the display in case the live preview or something
  // needs it
  $display->add_css = $css;

  $output = "<div class=\"panel-flexible " . $renderer->base['canvas'] . " clearfix\" $renderer->id_str>\n";
  $output .= "<div class=\"panel-flexible-inside " . $renderer->base['canvas'] . "-inside\">\n";

  $output .= mitema_panels_flexible_render_items($renderer, $settings['items']['canvas']['children'], $renderer->base['canvas']);

  // Wrap the whole thing up nice and snug
  $output .= "</div>\n</div>\n";

  return $output;
}

Ahora, creamos la función mitema_panels_flexible_render_items (llamada desde mitema_panels_flexible)

function mitema_panels_flexible_render_items($renderer, $list, $owner_id) {
  $output = '';
  $groups = array('left' => '', 'middle' => '', 'right' => '');
  $max = count($list) - 1;
  $prev = NULL;

  foreach ($list as $position => $id) {
    $item = $renderer->settings['items'][$id];
    $location = isset($renderer->positions[$id]) ? $renderer->positions[$id] : 'middle';

    if ($renderer->admin && $item['type'] != 'row' && $prev ) {
      $groups[$location] .= panels_flexible_render_splitter($renderer, $prev, $id);
    }

    switch ($item['type']) {
      
      case 'column':
        $content = mitema_panels_flexible_render_items($renderer, $item['children'], $renderer->base['column'] . '-' . $id);
        if (empty($renderer->settings['items'][$id]['hide_empty']) || trim($content)) {
          $groups[$location] .= mitema_panels_flexible_render_item($renderer, $item, $content, $id, $position, $max);
        }
        break;
      case 'row':
        $content = mitema_panels_flexible_render_items($renderer, $item['children'], $renderer->base['row'] . '-' . $id);
        if (empty($renderer->settings['items'][$id]['hide_empty']) || trim($content)) {
          $groups[$location] .= mitema_panels_flexible_render_item($renderer, $item, $content, $id, $position, $max, TRUE);
        }
        break;
      case 'region':
        if (empty($renderer->settings['items'][$id]['hide_empty'])) {
          $content = isset($renderer->content[$id]) ? $renderer->content[$id] : "&nbsp;";
        }
        else {
          $content = isset($renderer->content[$id]) ? trim($renderer->content[$id]) : "";
        }
        if (empty($renderer->settings['items'][$id]['hide_empty']) || $content) {
          $groups[$location] .= mitema_panels_flexible_render_item($renderer, $item, $content, $id, $position, $max);
        }
        break;
    }

    // If all items are fixed then we have a special splitter on the right to
    // control the overall width.
    if (!empty($renderer->admin) && $max == $position && $location == 'left') {
      $groups[$location] .= panels_flexible_render_splitter($renderer, $id, NULL);
    }
    $prev = $id;
  }

  $group_count = count(array_filter($groups));

  // Render each group. We only render the group div if we're in admin mode
  // or if there are multiple groups.
  foreach ($groups as $position => $content) {
    if (!empty($content) || $renderer->admin) {
      if ($group_count > 1 || $renderer->admin) {
        $output .= '<div class="' . $owner_id . '-' . $position . '">' . $content . '</div>';
      }
      else {
        $output .= $content;
      }
    }
  }

  return $output;
}

Y por último, la función mitema_panels_flexible_render_item

function mitema_panels_flexible_render_item($renderer, $item, $content, $id, $position, $max, $clear = FALSE) {

  switch($item['type']){
    case 'column':
      $class = "container";
      break;
    case 'row':
      $class = "row";
      break;
    case 'region':
      if (empty($item['class'])) {
        $class = "col-xs-12 col-lg-" . 12/($max+1);
      }
      break;
  }
  

  // If we are rendering a row and there is just one row, we don't need to
  // render the row unless there is fixed_width content inside it.
  /*
  if (empty($renderer->admin) && $item['type'] == 'row' && $max == 0) {
    $fixed = FALSE;
    foreach ($item['children'] as $id) {
      if ($renderer->settings['items'][$id]['width_type'] != '%') {
        $fixed = TRUE;
        break;
      }
    }

    if (!$fixed) {
      return $content;
    }
  }
  */

  // If we are rendering a column and there is just one column, we don't
  // need to render the column unless it has a fixed_width.
  if (empty($renderer->admin) && $item['type'] == 'column' && $max == 0 && $item['width_type'] == '%') {
    return $content;
  }

  $base = $renderer->item_class[$item['type']];
  $output = '<div class="' . $class . ' ' . $base . ' ' . $renderer->base[$item['type']] . '-' . $id;
  if ($position == 0) {
    $output .= ' ' . $base . '-first';
  }
  if ($position == $max) {
    $output .= ' ' . $base . '-last';
  }
  if ($clear) {
    $output .= ' clearfix';
  }

  if (isset($item['class'])) {
    $output .= ' ' . check_plain($item['class']);
  }

  $output .= '">' . "\n";

  if (!empty($renderer->admin)) {
    $output .= panels_flexible_render_item_links($renderer, $id, $item);
  }

  $output .= $content;
  $output .= '</div>' . "\n";

  return $output;
}

En el código se presenta lo mínimo indispensable para "limpiar" la cantidad de divs con millones de classes que pone en modo predeterminado Panels. Se puede hacer mejor, si hace falta. Porque un día alguien más inteligente terminará de desarrollar Panels Bootstrap Layout Builder.

Enchapado en oro: Modificar el css de panels dentro el theme admin

Ya tenemos nuestro layout y vemos que funciona a la perfección. El único problema es que mientras administramos un panel, el layout builder se presenta como quiere builder, en su forma orifginal. En mi caso (uso el theme ember) opté por agregar un archivo custom.css dentro del theme admin y lo agregué con un hook en el form alter de panels.. Más o menos así:

function custom_functions_form_ctools_export_ui_edit_item_wizard_form_alter(&$form, &$form_state, $form_id){
  global $theme_path;
  $css_file = $theme_path . "/styles/custom.css";
  if (file_exists($css_file)) {
    drupal_add_css($css_file);
  }
}

y el archivo custom.css:

.panels-flexible-splitter {
  display: none;
}

.panels-flexible-column {
  width: 100%;
  clear: both;
}

Hasta la próxima!