Skip to content

Commit

Permalink
Initial build-out
Browse files Browse the repository at this point in the history
  • Loading branch information
tkiehne committed Aug 21, 2024
1 parent 3631ac5 commit d2617fa
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 0 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Bootstrap Views Cards

Adds a views plugin to render outputs as Bootstrap 5 cards

## Requirements

- Views (Drupal core)
- Bootstrasp 5 based theme (technically optional - you can implement your own styling if you prefer)

## Features

This Views plugin supports most [Bootstrap 5 card](https://getbootstrap.com/docs/5.0/components/card) features:

- Header
- Footer
- Image
- Card body with:
- Title
- Subtitle
- Text

You may use template overrides and/or preprocess functions to customize further.
7 changes: 7 additions & 0 deletions bootstrap_views_cards.info.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: Bootstrap Views Cards
type: module
description: Adds a views plugin to render outputs as Bootstrap 5 cards.
package: Views
core_version_requirement: ^10.2 || ^11
dependencies:
- drupal:views
77 changes: 77 additions & 0 deletions bootstrap_views_cards.module
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

/**
* @file
* Custom functions for Bootstrap Views Cards.
*/

use Drupal\Component\Utility\Html;
use Drupal\Core\Template\Attribute;

/**
* Prepares variables for views cards templates.
*
* Default template: bootstrap-views-cards.html.twig.
*
* @param array $vars
* An associative array containing:
* - view: A ViewExecutable object.
* - rows: The raw row data.
*/
function template_preprocess_bootstrap_views_cards(array &$vars): void {
$view = $vars['view'];
$id = $view->storage->id() . '-' . $view->current_display;
$vars['id'] = Html::getUniqueId('bootstrap-views-cards-' . $id);

$vars['attributes'] = new Attribute(['class' => ['row', 'row-cols-1']]);
$cols = $view->style_plugin->options['card_group_columns'];
if ($cols > 1) {
switch ($cols) {
case 2:
$vars['attributes']->addClass('row-cols-sm-2');
break;

case 3:
$vars['attributes']->addClass('row-cols-sm-2 row-cols-md-3');
break;

case 4:
$vars['attributes']->addClass('row-cols-sm-2 row-cols-md-4');
break;
}
}

$options = $view->style_plugin->options;
if ($options['card_group_class_custom']) {
$option_classes = array_filter(explode(' ', $options['card_group_class_custom']));
$classes = array_map([Html::class, 'cleanCssIdentifier'], $option_classes);
$vars['attributes']->addClass($classes);
}

$vars['image_option'] = $view->style_plugin->options['card_image_option_field'];
$bodyClass = $vars['image_option'] == 'background' ? 'card-img-overlay' : 'card-body';
$vars['body_attributes'] = new Attribute(['class' => [$bodyClass]]);

// Card rows.
$header = $view->style_plugin->options['card_header_field'];
$image = $view->style_plugin->options['card_image_field'];
$title = $view->style_plugin->options['card_title_field'];
$subtitle = $view->style_plugin->options['card_subtitle_field'];
$content = $view->style_plugin->options['card_body_field'];
$footer = $view->style_plugin->options['card_footer_field'];

foreach ($vars['rows'] as $id => $row) {
$vars['rows'][$id] = [];
$vars['rows'][$id]['header'] = $view->style_plugin->getField($id, $header);
$vars['rows'][$id]['image'] = $view->style_plugin->getField($id, $image);
$vars['rows'][$id]['title'] = $view->style_plugin->getField($id, $title);
$vars['rows'][$id]['subtitle'] = $view->style_plugin->getField($id, $subtitle);
$vars['rows'][$id]['content'] = $view->style_plugin->getField($id, $content);
$vars['rows'][$id]['footer'] = $view->style_plugin->getField($id, $footer);
$vars['rows'][$id]['attributes'] = new Attribute(['class' => ['card', 'h-100']]);
if ($row_class = $view->style_plugin->getRowClass($id)) {
$vars['rows'][$id]['attributes']->addClass($row_class);
}

}
}
26 changes: 26 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "drupal/bootstrap_views_cards",
"description": "Adds a views plugin to render outputs as Bootstrap 5 cards.",
"type": "drupal-module",
"homepage": "https://github.com/deohs/bootstrap_views_cards",
"authors": [
{
"name": "Thomas Kiehne (tkiehne)",
"email": "[email protected]",
"homepage": "https://www.drupal.org/u/tkiehne",
"role": "Creator"
}
],
"support": {
"issues": "https://github.com/deohs/bootstrap_views_cards/issues",
"source": "https://github.com/deohs/bootstrap_views_cards"
},
"require": {
"php": ">=8.1"
},
"conflict": {
"drush/drush": "<10.2"
},
"license": "GPL-3.0-or-later",
"minimum-stability": "dev"
}
31 changes: 31 additions & 0 deletions config/schema/bootstrap_views_cards.style.schema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
views.style.bootstrap_views_cards:
type: views_style
label: 'Bootstrap views cards'
mapping:
card_header_field:
type: label
label: "Card header field"
card_title_field:
type: label
label: "Card title field"
card_subtitle_field:
type: label
label: "Card subtitle field"
card_body_field:
type: label
label: "Card body field"
card_image_field:
type: label
label: "Card image field"
card_image_option_field:
type: label
label: "Card image option field"
card_footer_field:
type: label
label: "Card footer field"
card_group_class_custom:
type: label
label: "Custom card group class"
card_group_columns:
type: label
label: "Cards per row"
170 changes: 170 additions & 0 deletions src/Plugin/views/style/BootstrapViewsCards.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

namespace Drupal\bootstrap_views_cards\Plugin\views\style;

use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\style\StylePluginBase;

/**
* Style plugin to render each item in a Bootstrap card.
*
* @ingroup views_style_plugins
*
* @ViewsStyle(
* id = "bootstrap_views_cards",
* title = @Translation("Bootstrap Cards"),
* help = @Translation("Displays rows in a Bootstrap Card Group layout"),
* theme = "bootstrap_views_cards",
* display_types = {"normal"}
* )
*/
class BootstrapViewsCards extends StylePluginBase {
/**
* Does the style plugin for itself support to add fields to it's output.
*
* @var bool
*/
protected $usesFields = TRUE;

/**
* Does the style plugin allows to use style plugins.
*
* @var bool
*/
protected $usesRowPlugin = TRUE;

/**
* Overrides \Drupal\views\Plugin\views\style\StylePluginBase::usesRowClass.
*
* @var bool
*/
protected $usesRowClass = TRUE;

/**
* Definition.
*/
protected function defineOptions() {
$options = parent::defineOptions();
unset($options['grouping']);
$options['card_header_field'] = ['default' => NULL];
$options['card_image_field'] = ['default' => NULL];
$options['card_image_option_field'] = ['default' => 'top'];
$options['card_title_field'] = ['default' => NULL];
$options['card_subtitle_field'] = ['default' => NULL];
$options['card_body_field'] = ['default' => NULL];
$options['card_footer_field'] = ['default' => NULL];
$options['card_group_class_custom'] = ['default' => NULL];
$options['card_group_columns'] = ['default' => 3];
return $options;
}

/**
* Render the given style.
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
unset($form['grouping']);
$form['help'] = [
'#markup' => $this->t('The Bootstrap cards displays content in a flexible container (<a href=":docs">see documentation</a>). Note that any fields not assigned below will not be displayed.',
[':docs' => 'https://getbootstrap.com/docs/5.0/components/card/']),
'#weight' => -99,
];
$form['card_header_field'] = [
'#type' => 'select',
'#title' => $this->t('Card header field'),
'#options' => $this->displayHandler->getFieldLabels(TRUE),
'#empty_option' => $this->t('- Select -'),
'#required' => FALSE,
'#default_value' => $this->options['card_header_field'],
'#description' => $this->t('Select the field that will be used for the card header. HTML tags will be stripped.'),
'#weight' => 1,
];
$form['card_image_field'] = [
'#type' => 'select',
'#title' => $this->t('Card image field'),
'#options' => $this->displayHandler->getFieldLabels(TRUE),
'#empty_option' => $this->t('- Select -'),
'#required' => FALSE,
'#default_value' => $this->options['card_image_field'],
'#description' => $this->t('Select the field that will be used for the card image.'),
'#weight' => 2,
];
$form['card_image_option_field'] = [
'#type' => 'select',
'#title' => $this->t('Image placement'),
'#description' => $this->t('Where to place the image in the card.'),
'#options' => [
'top' => $this->t('Top'),
'bottom' => $this->t('Bottom'),
'background' => $this->t('Background'),
],
'#default_value' => $this->options['card_image_option_field'],
'#weight' => 3,
];
$form['card_title_field'] = [
'#type' => 'select',
'#title' => $this->t('Card title field'),
'#options' => $this->displayHandler->getFieldLabels(TRUE),
'#empty_option' => $this->t('- Select -'),
'#required' => FALSE,
'#default_value' => $this->options['card_title_field'],
'#description' => $this->t('Select the field that will be used for the card title. HTML tags will be stripped.'),
'#weight' => 4,
];
$form['card_subtitle_field'] = [
'#type' => 'select',
'#title' => $this->t('Card subtitle field'),
'#options' => $this->displayHandler->getFieldLabels(TRUE),
'#empty_option' => $this->t('- Select -'),
'#required' => FALSE,
'#default_value' => $this->options['card_subtitle_field'],
'#description' => $this->t('Select the field that will be used for the card subtitle. HTML tags will be stripped.'),
'#weight' => 5,
];
$form['card_body_field'] = [
'#type' => 'select',
'#title' => $this->t('Card content field'),
'#options' => $this->displayHandler->getFieldLabels(TRUE),
'#empty_option' => $this->t('- Select -'),
'#required' => TRUE,
'#default_value' => $this->options['card_body_field'],
'#description' => $this->t('Select the field that will be used for the card content body.'),
'#weight' => 6,
];
$form['card_footer_field'] = [
'#type' => 'select',
'#title' => $this->t('Card footer field'),
'#options' => $this->displayHandler->getFieldLabels(TRUE),
'#empty_option' => $this->t('- Select -'),
'#required' => FALSE,
'#default_value' => $this->options['card_footer_field'],
'#description' => $this->t('Select the field that will be used for the card footer.'),
'#weight' => 7,
];
$form['card_group_class_custom'] = [
'#title' => $this->t('Custom card group class'),
'#description' => $this->t('Additional classes to provide on the card group, separated by a space.'),
'#type' => 'textfield',
'#required' => FALSE,
'#default_value' => $this->options['card_group_class_custom'],
'#weight' => 8,
];
$form['row_class']['#title'] = $this->t('Custom card class(es), separated by a space.');
$form['row_class']['#weight'] = 9;
$form['card_group_columns'] = [
'#type' => 'select',
'#title' => $this->t('Cards per row'),
'#description' => $this->t('The max number of cards to include in a row in the largest responsive viewport.'),
'#options' => [
1 => 1,
2 => 2,
3 => 3,
4 => 4,
],
'#required' => TRUE,
'#default_value' => $this->options['card_group_columns'],
'#weight' => 10,
];
}

}
51 changes: 51 additions & 0 deletions templates/bootstrap-views-cards.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{#
/**
* @file bootstrap-views-cards.html.twig
* Default simple view template to display Bootstrap 5 Cards.
*
*
* - rows: Contains a nested array of rows. Each row contains an array of
* columns.
* - row.attributes: Class attributes to apply to each card.
* - attributes: Class attributes for the card group as a whole.
* - image_option: The configured option for modifying image placement.
*
* @ingroup views_templates
*/
#}

<div {{ attributes }}>
{% for row in rows %}
<div class="col">
<div {{ row.attributes }}>
{% if row.header %}
<h5 class="card-header">
{{ row.header|striptags('<a><span><sup><sub>')|raw }}
</h5>
{% endif %}
{% if row.image and image_option != 'bottom' %}
<span class="{{ image_option == 'background' ? 'card-img' : 'card-img-top' }}">{{ row.image }}</span>
{% endif %}
<div {{ body_attributes }}>
{% if row.title %}
<h5 class="card-title">{{ row.title|striptags('<a><span><sup><sub>')|raw }}</h5>
{% endif %}
{% if row.subtitle %}
<h6 class="card-subtitle">{{ row.subtitle|striptags('<a><span><sup><sub>')|raw }}</h6>
{% endif %}
{% if row.content %}
{{- row.content -}}
{% endif %}
</div>
{% if row.image and image_option == 'bottom' %}
<span class="card-img-bottom">{{ row.image }}</span>
{% endif %}
{% if row.footer %}
<div class="card-footer">
{{ row.footer }}
</div>
{% endif %}
</div>
</div>
{%- endfor %}
</div>

0 comments on commit d2617fa

Please sign in to comment.