diff --git a/README.md b/README.md new file mode 100644 index 0000000..2eb16bc --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/bootstrap_views_cards.info.yml b/bootstrap_views_cards.info.yml new file mode 100644 index 0000000..fdae2e3 --- /dev/null +++ b/bootstrap_views_cards.info.yml @@ -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 diff --git a/bootstrap_views_cards.module b/bootstrap_views_cards.module new file mode 100644 index 0000000..1f29388 --- /dev/null +++ b/bootstrap_views_cards.module @@ -0,0 +1,77 @@ +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); + } + + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..52f483c --- /dev/null +++ b/composer.json @@ -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": "tkiehne@uw.edu", + "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" +} \ No newline at end of file diff --git a/config/schema/bootstrap_views_cards.style.schema.yml b/config/schema/bootstrap_views_cards.style.schema.yml new file mode 100644 index 0000000..8caaf38 --- /dev/null +++ b/config/schema/bootstrap_views_cards.style.schema.yml @@ -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" diff --git a/src/Plugin/views/style/BootstrapViewsCards.php b/src/Plugin/views/style/BootstrapViewsCards.php new file mode 100644 index 0000000..39edafc --- /dev/null +++ b/src/Plugin/views/style/BootstrapViewsCards.php @@ -0,0 +1,170 @@ + 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 (see documentation). 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, + ]; + } + +} diff --git a/templates/bootstrap-views-cards.html.twig b/templates/bootstrap-views-cards.html.twig new file mode 100644 index 0000000..43e429c --- /dev/null +++ b/templates/bootstrap-views-cards.html.twig @@ -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 + */ +#} + +