visualn-8.x-1.x-dev/modules/visualn_views/src/Plugin/views/style/VisualNDrawing.php
modules/visualn_views/src/Plugin/views/style/VisualNDrawing.php
<?php
namespace Drupal\visualn_views\Plugin\views\style;
use Drupal\rest\Plugin\views\style\Serializer;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\visualn\Manager\DrawerManager;
use Drupal\visualn\Manager\RawResourceFormatManager;
use Symfony\Component\Serializer\SerializerInterface;
use Drupal\core\form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Render\Element;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\visualn\BuilderService;
use Drupal\Core\Render\RenderContext;
/**
* Style plugin to render listing.
*
* @ingroup views_style_plugins
*
* @ViewsStyle(
* id = "visualn_drawing",
* title = @Translation("VisualN Drawing"),
* help = @Translation("Render a listing of view data."),
* display_types = { "normal" }
* )
*
*/
class VisualNDrawing extends Serializer {
const RAW_RESOURCE_FORMAT = 'visualn_generic_data_array';
/**
* {@inheritdoc}
*/
protected $usesRowPlugin = FALSE;
/**
* {@inheritdoc}
*/
protected $usesFields = TRUE;
/**
* The image style entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $visualNStyleStorage;
/**
* The visualn drawer manager service.
*
* @var \Drupal\visualn\Manager\DrawerManager
*/
protected $visualNDrawerManager;
/**
* The visualn resource format manager service.
*
* @var \Drupal\visualn\Manager\RawResourceFormatManager
*/
protected $visualNResourceFormatManager;
/**
* The visualn builder service.
*
* @var \Drupal\visualn\BuilderService
*/
protected $visualNBuilder;
/**
* The visualn unique identifier. Used for fields mapping and html_selector
* to distinguish from other drawings.
*
* @var string
*/
protected $vuid;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
// services used by serializer
$container->get('serializer'),
$container->getParameter('serializer.formats'),
$container->getParameter('serializer.format_providers'),
// services used by visauln_drawing itself
$container->get('entity_type.manager')->getStorage('visualn_style'),
$container->get('plugin.manager.visualn.drawer'),
$container->get('plugin.manager.visualn.raw_resource_format'),
$container->get('visualn.builder')
);
}
/**
* Constructs a Plugin object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition,
SerializerInterface $serializer, array $serializer_formats, array $serializer_format_providers,
EntityStorageInterface $visualn_style_storage, DrawerManager $visualn_drawer_manager, RawResourceFormatManager $visualn_resource_format_manager,
BuilderService $visualn_builder) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer, $serializer_formats, $serializer_format_providers);
//$this->definition = $plugin_definition + $configuration; // initialized also in parent::_construct()
$this->visualNStyleStorage = $visualn_style_storage;
$this->visualNDrawerManager = $visualn_drawer_manager;
$this->visualNResourceFormatManager = $visualn_resource_format_manager;
$this->visualNBuilder = $visualn_builder;
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['visualn_style_id'] = ['default' => ''];
$options['drawer_config'] = ['default' => []];
$options['expose_keys_mapping'] = 0;
$options['drawer_fields'] = ['default' => []];
return $options;
}
/**
* Get display style options.
*
* By default this returns $this->options, but can be overriden
* e.g. by exposed keys mapping form.
*
* @todo: add to class interface
*/
public function getVisualNOptions() {
// @todo: is that ok to change $this->options directly instead of copying and changing a new variable?
// @todo: check exposed mappings and override if any
// @todo: rename option key
if ($this->options['expose_keys_mapping']) {
// @todo:
//dsm($this->view->getExposedInput());
$exposed_input = $this->view->getExposedInput();
// @todo: the key should be unique. see visualn_form_alter()
// do not confuse with 'drawer_fields' in buildOptionsForm()
if (!empty($exposed_input['drawer_fields'])) {
foreach ($exposed_input['drawer_fields'] as $key => $input_value) {
// @todo: this one is actually form buildOptionsForm()
$this->options['drawer_fields'][$key] = $input_value['field'];
}
}
}
return $this->options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
// hide Serializer option to select format, only json is used to build Resource
$form['formats']['#access'] = FALSE;
//$visualn_styles = visualn_style_options(FALSE);
$visualn_styles = visualn_style_options();
$description_link = Link::fromTextAndUrl(
$this->t('Configure VisualN Styles'),
Url::fromRoute('entity.visualn_style.collection')
);
// @see ImageFormatter::settingsForm()
// @todo: onChange execute an ajax callback to show mappings form for the drawer
$form['visualn_style_id'] = array(
'#type' => 'select',
'#title' => $this->t('VisualN style'),
'#description' => $this->t('Default style for the data to render.'),
'#default_value' => $this->options['visualn_style_id'],
'#options' => $visualn_styles,
// @todo: add permission check for current user
'#description' => $description_link->toRenderable() + [
//'#access' => $this->currentUser->hasPermission('administer visualn styles')
'#access' => TRUE
],
'#ajax' => [
'url' => views_ui_build_form_url($form_state),
],
//'#executes_submit_callback' => TRUE,
'#required' => TRUE,
);
$form['drawer_container'] = [
'#type' => 'container',
'#process' => [[$this, 'processDrawerContainerSubform']],
];
// @todo: add a checkbox to choose whether to override default drawer config or not
// or an option to reset to defaults
// @todo: add group type of fieldset with info about overriding style drawer config
$form['expose_keys_mapping'] = [
'#type' => 'checkbox',
'#title' => $this->t('Expose fields mapping'),
'#default_value' => $this->options['expose_keys_mapping'],
];
}
public function processDrawerContainerSubform(array $element, FormStateInterface $form_state, $form) {
// @todo: it seems that that most code here could be moved outside into a method since it is used multiple times
// in other places (see ResourceGenericDrawingFetcher::processDrawerContainerSubform() for example)
$element_parents = $element['#parents'];
// Here form_state corresponds to the current display style handler though is not instanceof SubformStateInterface.
$style_element_parents = array_slice($element['#parents'], 0, -1);
// since the function if called as a #process callback and the visualn_style_id select was already processed
// and the values were mapped then it is enough to get form_state value for it and no need to check
// configuration value (see FormBuilder::processForm() and FormBuilder::doBuildForm())
// and no need in "is_null($visualn_style_id) then set value from config"
$visualn_style_id = $form_state->getValue(array_merge($style_element_parents, ['visualn_style_id']));
// If it is a fresh form (is_null($visualn_style_id)) or an empty option selected ($visualn_style_id == ""),
// there is nothing to attach for drawer config.
if (!$visualn_style_id) {
return $element;
}
// Here the drawer plugin is initialized (inside getDrawerPlugin()) with the config stored in the style.
$drawer_plugin = $this->visualNStyleStorage->load($visualn_style_id)->getDrawerPlugin();
// We use drawer config from configuration only if it corresponds to the selected style. Also
// we don't get form_state values for the drawer config here since they are handled by
// drawer buildConfigurationForm() method itself and also even in buildConfigurationForm()
// drawer should have access to the $this->configuration['drawer_config'] values.
if ($visualn_style_id == $this->options['visualn_style_id']) {
// Set initial configuration for the plugin according to the configuration stored in fetcher config.
$drawer_config = $this->options['drawer_config'];
$drawer_plugin->setConfiguration($drawer_config);
// @todo: uncomment when the issue with handling drawer fields form_state values is resolved.
//$drawer_fields = $this->options['drawer_fields'];
// @todo: Until some generic way to hande drawer_fields form is introduced,
// e.g. \VisualN::buildDrawerDataKeysForm(), we should handle form_state values for the drawer_fields
// manually (i.e. in case of form validation errors form_state values should be used).
$drawer_fields
= $form_state->getValue(array_merge($element_parents, ['drawer_fields']), $this->options['drawer_fields']);
}
else {
// Leave drawer_config unset for later initialization with drawer_plugin->getConfiguration() values
// which are generally taken from visualn style configuration.
// Initialize drawer_config based on (visualn style stored config) in case it is needed somewhere else below.
$drawer_config = $drawer_plugin->getConfiguration();
// Since drawer_fields is always an empty array for a visualn style drawer plugin (VisualNStyle::getDrawerPlugin()),
// it is ok to set it to an empty array here. In contrast, if null, drawer_config should be taken
// from the visualn style plugin configuraion.
$drawer_fields = [];
}
// Use unique drawer container key for each visualn style from the select box so that the settings
// wouldn't be overridden by the previous one on ajax calls (expecially when styles use the same
// drawer and thus the same configuration form with the same keys).
$drawer_container_key = $visualn_style_id;
// get drawer configuration form
$element[$drawer_container_key]['drawer_config'] = [];
$element[$drawer_container_key]['drawer_config'] += [
'#parents' => array_merge($element['#parents'], [$drawer_container_key, 'drawer_config']),
'#array_parents' => array_merge($element['#array_parents'], [$drawer_container_key, 'drawer_config']),
];
$subform_state = SubformState::createForSubform($element[$drawer_container_key]['drawer_config'], $form, $form_state);
// attach drawer configuration form
$element[$drawer_container_key]['drawer_config']
= $drawer_plugin->buildConfigurationForm($element[$drawer_container_key]['drawer_config'], $subform_state);
// Process #ajax elements. If drawer configuration form uses #ajax to rebuild elements on cerain events,
// those calls must use views specific 'url' setting or new elements values won't be saved.
$this->replaceAjaxOptions($element[$drawer_container_key]['drawer_config'], $form_state);
// @todo: Use some kind of \VisualN::buildDrawerDataKeysForm($drawer_plugin, $form, $form_state) here.
// Drawer fields subform (i.e. data_keys mappings) should be attached in a separate #process callback
// that would trigger after the drawer buildConfigurationForm() attaches the config form
// and is completed. It is required for the case when drawer has a variable number of data keys.
// see the code below
// @see VisualNFormsHelper::processDrawerContainerSubform()
$element[$drawer_container_key]['drawer_fields']['#process'] = [[get_called_class(), 'processDrawerFieldsSubform']];
$element[$drawer_container_key]['drawer_fields']['#drawer_plugin'] = $drawer_plugin;
$element[$drawer_container_key]['drawer_fields']['#drawer_fields'] = $drawer_fields;
$field_names = $this->displayHandler->getFieldLabels();
$element[$drawer_container_key]['drawer_fields']['#field_names'] = $field_names;
// @todo: technically, this should be moved to an #after_build callback (though it seems to work even as it is)
// @see VisualNFormsHelper::processDrawerContainerSubform() for more info
// since drawer and fields onfiguration forms may be empty, do a check (then it souldn't be of details type)
if (Element::children($element[$drawer_container_key]['drawer_config'])
|| Element::children($element[$drawer_container_key]['drawer_fields'])) {
$details_open = FALSE;
if ($form_state->getTriggeringElement()) {
$style_element_array_parents = array_slice($element['#array_parents'], 0, -1);
// check that the triggering element is visualn_style_id but not fetcher_id select (or some other element) itself
$triggering_element = $form_state->getTriggeringElement();
// @todo: triggering element may be empty
$details_open = $triggering_element['#array_parents'] === array_merge($style_element_array_parents, ['visualn_style_id']);
// if triggered an ajaxafield configuration form element, open configuration form details after refresh
if (!$details_open) {
$array_merge = array_merge($element['#array_parents'], [$drawer_container_key, 'drawer_config']);
$array_diff = array_diff($triggering_element['#array_parents'], $array_merge);
$is_subarray = $triggering_element['#array_parents'] == array_merge($array_merge, $array_diff);
if ($is_subarray) {
$details_open = TRUE;
}
}
}
$element[$drawer_container_key] = [
'#type' => 'details',
'#title' => t('Style configuration'),
'#open' => $details_open,
] + $element[$drawer_container_key];
}
// @todo: attach #element_validate
return $element;
}
/**
* Process #ajax elements. If drawer configuration form uses #ajax to rebuild elements on cerain events,
* those calls must use views specific 'url' setting or new elements values won't be saved.
*/
protected function replaceAjaxOptions(&$element, FormStateInterface $form_state) {
foreach (Element::children($element) as $key) {
if (isset($element[$key]['#ajax'])) {
$element[$key]['#ajax'] = ['url' => views_ui_build_form_url($form_state)];
}
// check subtree elements
$this->replaceAjaxOptions($element[$key], $form_state);
}
}
/**
* {@inheritdoc}
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state) {
parent::submitOptionsForm($form, $form_state);
//$drawer_container_key = reset(Element::children($form['drawer_container']));
$drawer_container_key = Element::children($form['drawer_container'])[0];
//$base_element_parents = array_slice($element_parents, 0, -1);
$element_parents = array_merge($form['#parents'], ['drawer_container']);
$base_element_parents = $form['#parents'];
// Call drawer_plugin submitConfigurationForm(),
// submitting should be done before $form_state->unsetValue() after restructuring the form_state values, see below.
$full_form = $form_state->getCompleteForm();
$subform = $form['drawer_container'][$drawer_container_key]['drawer_config'];
$sub_form_state = SubformState::createForSubform($subform, $full_form, $form_state);
$visualn_style_id = $form_state->getValue(array_merge($base_element_parents, ['visualn_style_id']));
$visualn_style = $this->visualNStyleStorage->load($visualn_style_id);
$drawer_plugin = $visualn_style->getDrawerPlugin();
$drawer_plugin->submitConfigurationForm($subform, $sub_form_state);
// move drawer_config two levels up (remove 'drawer_container' and $drawer_container_key) in form_state values
$drawer_config_values = $form_state->getValue(array_merge($element_parents, [$drawer_container_key, 'drawer_config']));
if (!is_null($drawer_config_values)) {
$form_state->setValue(array_merge($base_element_parents, ['drawer_config']), $drawer_config_values);
}
// move drawer_config two levels up (remove 'drawer_container' and $drawer_container_key) in form_state values
$drawer_fields_values = $form_state->getValue(array_merge($element_parents, [$drawer_container_key, 'drawer_fields']));
if (!is_null($drawer_fields_values)) {
$new_drawer_fields_values = [];
foreach ($drawer_fields_values as $drawer_field_key => $drawer_field) {
$new_drawer_fields_values[$drawer_field_key] = $drawer_field['field'];
}
$form_state->setValue(array_merge($base_element_parents, ['drawer_fields']), $new_drawer_fields_values);
}
// remove remove 'drawer_container' key itself from form_state
$form_state->unsetValue(array_merge($element_parents, [$drawer_container_key]));
}
// @todo: till this point it is mostly a copy-paste from Visualization style plugin
// with some changes to ::create() and ::__construct() methods
//
/**
* Prepare drawing complete build
*/
protected function doDrawingBuild($json_data) {
// generally this returns $this->options but can overridden
$style_options = $this->getVisualNOptions();
// @todo: since this can be cached it could not take style changes (i.e. made in style
// configuration interface) into consideration, so a cache tag may be needed.
$visualn_style_id = $style_options['visualn_style_id'];
if (empty($visualn_style_id)) {
return;
}
$drawer_config = $style_options['drawer_config'];
$drawer_fields = $style_options['drawer_fields'];
$raw_resource_plugin_id = static::RAW_RESOURCE_FORMAT;
$raw_input = [
'data' => $json_data,
];
$resource =
$this->visualNResourceFormatManager->createInstance($raw_resource_plugin_id, [])
->buildResource($raw_input);
// Get drawing build
$build = $this->visualNBuilder->makeBuildByResource($resource, $visualn_style_id, $drawer_config, $drawer_fields);
$this->visualNBuilder;
return $build;
}
/**
* Render JSON markup for further processing into a json array to
* be used by adapter and optionally shown in views preview
*/
public function renderJSON() {
// @note: This is mostly a copy-paste of \Drupal\rest\Plugin\views\style\Serializer::render()
// but without using row plugin
$rows = [];
// If the Data Entity row plugin is used, this will be an array of entities
// which will pass through Serializer to one of the registered Normalizers,
// which will transform it to arrays/scalars. If the Data field row plugin
// is used, $rows will not contain objects and will pass directly to the
// Encoder.
foreach ($this->view->result as $row_index => $row) {
$this->view->row_index = $row_index;
$rows[] = $this->renderRow($row);
//$rows[] = $this->view->rowPlugin
//->render($row);
}
unset($this->view->row_index);
// Get the content type configured in the display or fallback to the
// default.
if (empty($this->view->live_preview)) {
$content_type = !empty($this->options['formats']) ? reset($this->options['formats']) : 'json';
// @todo: doesn't work in normal view mode
//$content_type = $this->displayHandler
//->getContentType();
}
else {
$content_type = !empty($this->options['formats']) ? reset($this->options['formats']) : 'json';
}
return $this->serializer
->serialize($rows, $content_type, [
'views_style_plugin' => $this,
]);
}
/**
* {@inheritdoc}
*/
public function render() {
// @note: The code is based on \Drupal\rest\Plugin\views\display\RestExport::render()
// @todo: review this method implementation, it is a quickfix
$renderer = \Drupal::service('renderer');
$build = [];
//$build['#markup'] = $this->renderer
//$build['#markup'] = $this->view->renderer
$json_markup = $renderer
->executeInRenderContext(new RenderContext(), function () {
return $this->renderJSON();
//return $this->view->style_plugin
//->render();
});
// decode into an array
$json_data = json_decode($json_markup, TRUE);
if (!empty($this->view->live_preview)) {
$build['json_preview']['#markup'] = $json_markup;
}
$drawing_build = $this->doDrawingBuild($json_data);
$build['drawing_build'] = $drawing_build;
return $build;
}
/**
* {@inheritdoc}
*/
protected function renderRow($row) {
// @note: The code is based on \Drupal\rest\Plugin\views\row\DataFieldRow::render()
$output = [];
foreach ($this->view->field as $id => $field) {
// @todo: enable raw output option
// If the raw output option has been set, just get the raw value.
if (FALSE) {
//if (!empty($this->rawOutputOptions[$id])) {
$value = $field
->getValue($row);
}
else {
$value = $field
->advancedRender($row);
}
// Omit excluded fields from the rendered output.
if (empty($field->options['exclude'])) {
$output[$id] = $value;
//$output[$this
//->getFieldKeyAlias($id)] = $value;
}
}
return $output;
}
/**
* Attach drawer_fields subform based on drawer_plugin dataKeys().
*
* The subform is attached in a #process callback to have drawer config form
* values already mapped at this point. It is needed e.g. for drawers with variable
* number of data keys managed in a #process callback set in buildConfigurationForm().
*
* @see \Drupal\visualn_basic_drawers\Plugin\VisualN\Drawer\LinechartBasicDrawer
*/
public static function processDrawerFieldsSubform(array $element, FormStateInterface $form_state, $form) {
// this is mostly a copy-paste of the VisualNFormsHelper::processDrawerContainerSubform()
$field_names = $element['#field_names'];
$element_parents = $element['#array_parents'];
$base_element_parents = array_slice($element_parents, 0, -1);
$base_element_parents[] = 'drawer_config';
$config_element = NestedArray::getValue($form, $base_element_parents);
$subform_state = SubformState::createForSubform($config_element, $form, $form_state);
$drawer_plugin = $element['#drawer_plugin'];
$drawer_fields = $element['#drawer_fields'];
$drawer_plugin_clone = clone $drawer_plugin;
$drawer_config = $drawer_plugin->extractFormValues($config_element, $subform_state);
$drawer_plugin_clone->setConfiguration($drawer_config);
$data_keys = $drawer_plugin_clone->dataKeys();
// @todo: convert textfields into a table in a #process callback
// maybe even inside Mapper config form method
if (!empty($data_keys)) {
// @todo: get rid of value from 'field' or massage value at plugin submit
$element += [
'#type' => 'table',
'#header' => [t('Data key'), t('Field')],
];
foreach ($data_keys as $i => $data_key) {
$element[$data_key]['label'] = [
'#plain_text' => $data_key,
];
$element[$data_key]['field'] = [
'#type' => 'select',
'#options' => $field_names,
'#default_value' => isset($drawer_fields[$data_key]) ? $drawer_fields[$data_key] : '',
'#empty_option' => t('- Select data source -'),
'#required' => FALSE,
];
}
}
return $element;
}
}
