stacks-8.x-1.x-dev/modules/stacks_content_feed/src/Plugin/WidgetType/ContentFeed.php
modules/stacks_content_feed/src/Plugin/WidgetType/ContentFeed.php
<?php
namespace Drupal\stacks_content_feed\Plugin\WidgetType;
use Drupal\node\Entity\NodeType;
use Drupal\stacks\Plugin\WidgetTypeBase;
use Drupal\stacks_content_feed\StacksQuery\StacksDatabaseQuery;
use Drupal\stacks_content_feed\StacksQuery\StacksSolrQuery;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\AppendCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Template\Attribute;
use Drupal\taxonomy\Entity\Term;
/**
* ContentFeed.
*
* @WidgetType(
* id = "content_feed",
* label = @Translation("Content Feed"),
* )
*/
class ContentFeed extends WidgetTypeBase {
/**
* Responsible for the initial setup for the grid. This includes setting the
* entity and setting up the grid options for this grid type.
*
* @param array $configuration
* @param string $plugin_id
* @param mixed $plugin_definition
* @internal param $widget_entity (object): The stacks entity that has the options.
* This is sent here from Drupal\stacks_content_feed\Widget\WidgetData::output().
*
* $this->grid_options (array)
* content_types (array): Available content types to query from.
* vocabulary (string): Set a vocabulary machine name if you want to have a
* filter that pulls in all terms under a vocabulary.
* taxonomy_terms (array): Instead of setting a vocabulary, you can select
* certain taxonomy terms.
* order_by (string): How to order the query by default: title_asc,
* title_desc, created_asc, created_desc
* sort (array): Array of available sort options. If the order_by field
* comes from an option list, it usually makes sense to grab those options.
* enabled_filters (array): Which filters are enabled?
* pagination_type (string): Change the type of pagination that is used:
* default, mini, load more, load more scroll
* per_page (int): If using pagination, how many results per page?
* total_results (int): If not using pagination, how many results to display?
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->grid_options = [
'per_page' => $this->getStringFieldValue('field_cfeed_results_per_page'),
'content_types' => $this->getArrayFieldValues('field_cfeed_content_types'),
'vocabulary' => isset($this->getArrayFieldValues('field_cfeed_vocabulary')[0]) ? $this->getArrayFieldValues('field_cfeed_vocabulary') : FALSE,
'taxonomy_terms' => $this->getArrayFieldValues('field_cfeed_taxonomy_terms'),
'order_by' => $this->getStringFieldValue('field_cfeed_order'),
'sticky' => $this->getBooleanFieldValue('field_cfeed_sticky'),
'sort' => ($this->widget_entity->hasField('field_cfeed_order')) ? $this->widget_entity->get('field_cfeed_order')->getSetting('allowed_values') : 'DESC',
'enabled_filters' => $this->getArrayFieldValues('field_cfeed_enable_filtering'),
'pagination_type' => $this->getStringFieldValue('field_cfeed_pagination'),
'limit_by' => $this->getStringFieldValue('field_cfeed_limit_by'),
];
// If they select a vocabulary for the filter, do not limit the query by
// taxonomy terms!
if (!empty($this->grid_options['vocabulary'])) {
$this->grid_options['taxonomy_terms'] = [];
}
$this->setFiltersFrontend();
}
/**
* Modify the render array before output. This is used for the initial display
* and also all AJAX requests.
*/
public function modifyRenderArray(&$render_array, $options = []) {
$active_filters = isset($options['active_filters']) ? $options['active_filters'] : FALSE;
$is_ajax = isset($options['is_ajax']) ? $options['is_ajax'] : FALSE;
// Get the results.
$query_options = [
'status' => 1,
'content_types' => $this->grid_options['content_types'],
'taxonomy' => [
'ui_selected_tags' => $this->grid_options['taxonomy_terms'],
],
'per_page' => $this->grid_options['per_page'],
'order_by' => $this->grid_options['order_by'],
'sticky' => $this->grid_options['sticky'],
'active_filters' => $active_filters,
'pagination_type' => $this->grid_options['pagination_type'],
'limit_by' => $this->grid_options['limit_by'],
];
// Adds results, js/css, and variables to the render array.
$this->prepareNodeGridAjax($render_array, $is_ajax, $query_options);
}
/**
* If filters are sent, modify the $options array.
*
* We take the filters and override the $options in the array to only select
* the content they want.
* @param $options
*/
public function getNodeResultsActiveFilters(&$options) {
// Put together an array of all fields that are taxonomy fields on the
// content type(s).
$tag_fields = [];
if (isset($options['content_types']) && is_array($options['content_types']) && count($options['content_types'])) {
$taxonomy_helper = \Drupal::service('stacks.taxonomy_helper');
$tag_fields = $taxonomy_helper->getTaxonomyFieldsForContentTypes($options['content_types']);
if (count($tag_fields)) {
$options['taxonomy_terms_field_names'] = $tag_fields;
}
}
$active_filters = isset($options['active_filters']) ? $options['active_filters'] : [];
if (is_countable($active_filters)) {
if (count($active_filters) < 1) {
return;
}
}
// Content Types.
if (isset($active_filters['content_types']) && count($active_filters['content_types']) > 0) {
// Make sure the selected filter option is an available option.
foreach ($active_filters['content_types'] as $content_type) {
if (!isset($this->grid_options['filters']['content_types'][$content_type])) {
return;
}
}
$options['content_types'] = $active_filters['content_types'];
}
// Taxonomies.
// Only limit by taxonomy terms if the vocabulary option is not set under filters.
if (isset($active_filters['taxonomy']) && count($active_filters['taxonomy']) > 0) {
// Add the taxonomy term ids to an array that is categorized by taxonomy field.
foreach ($active_filters['taxonomy'] as $vocab => $tids) {
if (isset($tag_fields[$vocab])) {
foreach ($tag_fields[$vocab] as $field_name) {
if (!isset($options['taxonomy'][$field_name])) {
$options['taxonomy'][$field_name] = [];
}
$options['taxonomy'][$field_name] = array_merge($options['taxonomy'][$field_name], $tids);
}
}
} // End terms foreach
}
// Sort.
if (isset($active_filters['sort']) && count($active_filters['sort']) > 0) {
foreach ($active_filters['sort'] as $sort) {
// Make sure the selected filter option is an available option.
if (!isset($this->grid_options['sort'][$sort])) {
return;
}
$options['order_by'] = $sort;
} // End terms foreach
}
// Search.
if (isset($active_filters['search']) && !empty($active_filters['search'][0])) {
$options['search'] = trim($active_filters['search'][0]);
}
}
/**
* Queries the database for nodes.
*
* @param $options
* @param bool $is_ajax
* @return \Drupal\Core\Entity\EntityInterface[]
*/
public function getNodeResults($options, $is_ajax = FALSE) {
$use_solr = FALSE;
// Handle Filter Values.
$this->getNodeResultsActiveFilters($options);
if (\Drupal::moduleHandler()->moduleExists('search_api_solr')) {
$config = \Drupal::service('config.factory')
->getEditable('stacks.settings');
$search_api_index = $config->get("content_feed_search_api_index");
$fulltext_field = $config->get("content_feed_search_api_fulltext_field");
if ($search_api_index != NULL && $fulltext_field != NULL) {
$use_solr = TRUE;
}
}
if ($use_solr == TRUE) {
$stacks_query = new StacksSolrQuery($this->unique_id, $search_api_index, $fulltext_field);
}
else {
$stacks_query = new StacksDatabaseQuery($this->unique_id);
$stacks_query->setEntity($this->widget_entity);
}
$nids = $stacks_query->getNodeResults($options);
$data = \Drupal::entityTypeManager()
->getStorage('node')
->loadMultiple($nids);
return $data;
}
/**
* Takes a field name and returns a value if it is there. Otherwise NULL. This
* is meant to be used when a stacks entity is used.
* @param $field_name
* @param bool $entity
* @return string
*/
public function getStringFieldValue($field_name, $entity = FALSE) {
if (!$entity) {
$entity = $this->widget_entity;
}
if (!$entity->hasField($field_name)) {
return '';
}
$value = $entity->get($field_name)->getValue();
return isset($value[0]['value']) ? $value[0]['value'] : '';
}
/**
* Similar to getStringFieldValue(), but meant for boolean fields.
* @param $field_name
* @param bool $entity
* @return bool
*/
public function getBooleanFieldValue($field_name, $entity = FALSE) {
if (!$entity) {
$entity = $this->widget_entity;
}
if (!$entity->hasField($field_name)) {
return FALSE;
}
$field_value = $entity->get($field_name)->getValue();
if (isset($field_value[0]['value'])) {
$field_value = $field_value[0]['value'];
}
$value = FALSE;
if ($field_value) {
$value = TRUE;
}
return $value;
}
/**
* Takes a multidimensional array and returns an array of values.
* @param $field_name
* @param bool $entity
* @return array|bool
*/
public function getArrayFieldValues($field_name, $entity = FALSE) {
if (!$entity) {
$entity = $this->widget_entity;
}
if (!$entity->hasField($field_name)) {
return [];
}
$array = $entity->get($field_name)->getValue();
$clean_array = [];
foreach ($array as $value) {
if (is_array($value)) {
foreach ($value as $value2) {
$clean_array[] = $value2;
}
}
else {
$clean_array[] = $value;
}
}
if (count($clean_array) < 1) {
return FALSE;
}
return $clean_array;
}
/**
* Modify the render array for Content Feed widgets. This is hit when the page
* first loads and in all ajax requests for this grid.
*
* @param $render_array
* @param $is_ajax
*/
public function prepareGridAjax(&$render_array, $is_ajax) {
if (!$this->widget_entity && !$this->unique_id) {
// If $this->widget_entity is false, we need to make sure a unique
// widget id is set.
$this->messenger()->addError(t('Grid widget id need to be set.'));
return;
}
if ($this->widget_entity) {
// We need to set the entity object. Since WidgetData->output() isn't hit
// for with the ajax request, we need to set it.
$render_array['#widget_entity'] = [
'entity_id' => $this->widget_entity->id(),
'entity_type' => $this->widget_entity->getEntityTypeId(),
'entity_bundle' => $this->widget_entity->bundle(),
];
}
else {
// This is not a stacks entity. Set some values.
$render_array['#widget_entity'] = [
'entity_id' => $this->unique_id,
];
if (!$is_ajax) {
// Define which template to use. This should be set in child class.
$render_array['#theme'] = $this->template;
}
}
// If this is not an ajax request, we need to send ajax_attributes and
// attach the necessary JS/CSS. The key for each attribute should not
// contain underscores.
if (!$is_ajax) {
$render_array['#grid']['ajax_attributes'] = new Attribute([
'id' => 'grid_widget_' . $this->unique_id,
'widgetid' => $this->unique_id,
'typeofgrid' => 'contentfeed',
// Ajax request theme call.
'theme' => str_replace('contentfeed', 'ajax_contentfeed', $render_array['#theme']),
'isentity' => ($this->widget_entity) ? 1 : 0,
// If this is not a stacks entity, the value will be set by the class
// with the path to the object.
'notentity' => '',
]);
$render_array['#attached']['library'][] = 'stacks_content_feed/grid.ajax';
}
}
/**
* Call this method when returning nodes in your modifyRenderArray(). This
* adds the results, the necessary JS/CSS and variables for ajax calls.
* @param $render_array
* @param $is_ajax
* @param $query_options
*/
public function prepareNodeGridAjax(&$render_array, $is_ajax, $query_options) {
// Adds js/css, sets attributes, etc...
$this->prepareGridAjax($render_array, $is_ajax);
// Adding filters to set All terms when restricting terms.
if (empty($query_options['active_filters']) || count($query_options['active_filters']) == 0 || !isset($query_options['active_filters']['taxonomy'])) {
if ($this->widget_entity->hasField('field_cfeed_taxonomy_terms')) {
$cfeed_taxonomy_terms_field = $this->widget_entity->get('field_cfeed_taxonomy_terms')->getValue();
if (count($cfeed_taxonomy_terms_field) > 0) {
// Pulling the right taxonomy vocabularies to rebuild
// the filters when 'All' is selected.
foreach ($cfeed_taxonomy_terms_field as $tid) {
$term_detail = Term::load($tid['target_id']);
if (isset($term_detail)) {
$query_options['active_filters']['taxonomy'][$term_detail->bundle()][] = $tid['target_id'];
}
}
}
}
}
$render_array['#grid']['results'] = $this->getNodeResults($query_options, $is_ajax);
$render_array['#grid']['filters'] = isset($this->grid_options['filters']) ? $this->grid_options['filters'] : [];
$render_array['#grid']['default_sort'] = isset($this->grid_options['order_by']) ? $this->grid_options['order_by'] : 'title_asc';
$render_array['#grid']['pagination_type'] = isset($this->grid_options['pagination_type']) ? $this->grid_options['pagination_type'] : '';
$render_array['#attached']['drupalSettings']['stacksgrid']['scroll'] = [];
if ($render_array['#grid']['pagination_type'] == 'load_more_scroll') {
$render_array['#attached']['drupalSettings']['stacksgrid']['scroll'][$this->unique_id] = $render_array['#grid']['pagination_type'];
}
}
/**
* Is called with the ajax controller is hit.
*
* @See: Drupal\stacks_content_feed\\GridController
*
* @param $render_array
* @param $active_filters
* @return AjaxResponse() object.
*/
public function doAjax($render_array, $active_filters) {
$this->modifyRenderArray($render_array, [
'active_filters' => $active_filters,
'is_ajax' => TRUE,
]);
// This is the id of the ajax_results div.
// When you do ajax respond commands. It runs against the whole document.
$wrapper_id = '#grid_widget_' . $this->unique_id;
$response = new AjaxResponse();
if (isset($this->grid_options['pagination_type']) && ($this->grid_options['pagination_type'] == 'load_more' || $this->grid_options['pagination_type'] == 'load_more_scroll')) {
// Remove the previous load more button.
$response->addCommand(new InvokeCommand($wrapper_id . ' .ajax_results_pagination', 'remove'));
// Append Results
// Don't send a selector. Let the JS figure out where it should go. This
// should use gridAjaxSettings.wrapper set in the JS.
$response->addCommand(new AppendCommand(NULL, $render_array));
}
else {
// Replace Results
// Don't send a selector. Let the JS figure out where it should go. This
// should use gridAjaxSettings.wrapper set in the JS.
$response->addCommand(new HtmlCommand(NULL, $render_array));
}
// Trigger the equalizer js. See grid.ajax.js.
$response->addCommand(new InvokeCommand($wrapper_id, 'ajaxequalizer'));
return $response;
}
/**
* Puts together the arrays for the filters variable in the template.
*/
protected function setFiltersFrontend() {
$taxonomy_helper = \Drupal::service('stacks.taxonomy_helper');
$filters = [];
// Loop through the enabled filters. Grab the correct grid_option values
// and use that to populate the available options.
$enabled_filters = $this->grid_options['enabled_filters'];
if (is_array($enabled_filters)) {
foreach ($enabled_filters as $filter) {
// Loop through each option. Options are based on grid_options.
$filter_options = isset($this->grid_options[$filter]) ? $this->grid_options[$filter] : FALSE;
if ($filter_options && count($filter_options) > 0) {
foreach ($filter_options as $key => $filter_options_row) {
$value = ucwords($filter_options_row);
if ($filter == 'taxonomy_terms') {
// Load Taxonomy Term Title.
$term = Term::load($value);
$value = $term->getName();
}
elseif ($filter == 'content_types') {
$content_type = NodeType::load($filter_options_row);
$value = $content_type->get('name');
}
else {
if ($filter == 'sort') {
$filter_options_row = $key;
}
}
$filters[$filter][$filter_options_row] = $value;
} // End $filter_options foreach
}
else {
// These filters don't have options.
$filters[$filter] = 1;
} // End $filter_options if
} // End $enabled_filters foreach
} // End main if
// If taxonomy filter is enabled and if they set a vocabulary, populate
// the taxonomy terms from the vocabulary.
if ($this->grid_options['vocabulary'] && is_array($enabled_filters) && in_array('taxonomy_terms', $enabled_filters)) {
// Adding parameters to remove elements from the vocabulary select field
// if we don't use all the terms.
$cfeed_taxonomy_terms = [];
$cfeed_taxonomy_terms_field = $this->widget_entity->field_cfeed_taxonomy_terms->getValue();
if (count($cfeed_taxonomy_terms_field) > 0) {
foreach ($cfeed_taxonomy_terms_field as $taxonomy_term) {
$cfeed_taxonomy_terms[] = $taxonomy_term['target_id'];
}
}
foreach ($this->grid_options['vocabulary'] as $vocab) {
$taxonomy_helper->getTermsFromVocab($filters, $vocab, $cfeed_taxonomy_terms);
}
}
$this->grid_options['filters'] = $filters;
}
/**
* Helper function for returning an ajax request. This makes it simpler to
* debug AJAX issues. If user is logged in, displays $_POST variables.
* @param $message
* @return \Drupal\Core\Ajax\AjaxResponse
*/
static public function postAjaxErrorMessage($message) {
if (!\Drupal::currentUser()->isAnonymous()) {
$message .= '<pre>' . print_r($_POST, TRUE) . '</pre>';
}
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand(FALSE, ['#markup' => $message]));
return $response;
}
/**
* Define the fields that should not be sent to the template as variables.
* These are usually fields on the bundle that you want to handle via
* programming only.
*/
public function fieldExceptions() {
return [
'field_cfeed_content_types',
'field_cfeed_enable_filtering',
'field_cfeed_limit_by',
'field_cfeed_order',
'field_cfeed_pagination',
'field_cfeed_results_per_page',
'field_cfeed_sticky',
'field_cfeed_taxonomy_terms',
'field_cfeed_vocabulary',
];
}
}
