You are here

GridStackFormBase.php in GridStack 8.2

File

modules/gridstack_ui/src/Form/GridStackFormBase.php
View source
<?php

namespace Drupal\gridstack_ui\Form;

use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Template\Attribute;
use Drupal\gridstack\GridStackDefault;
use Drupal\gridstack\Entity\GridStack;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Extends base form for gridstack instance configuration form.
 */
class GridStackFormBase extends EntityForm {

  /**
   * The file system service.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * The blazy manager service.
   *
   * @var \Drupal\blazy\Form\BlazyAdminInterface
   */
  protected $blazyAdmin;

  /**
   * The GridStack manager service.
   *
   * @var \Drupal\gridstack\GridStackManagerInterface
   */
  protected $manager;

  /**
   * The GridStack default entity.
   *
   * @var \Drupal\gridstack\Entity\GridStack
   */
  protected $default;

  /**
   * The flag whether the admin CSS is enabled, or not.
   *
   * @var bool
   */
  protected $adminCss;

  /**
   * The flag whether the entity is default, or not.
   *
   * @var bool
   */
  protected $isDefault;

  /**
   * The flag whether the nested option (framework) is enabled, or not.
   *
   * @var bool
   */
  protected $useNested = FALSE;

  /**
   * The active CSS framework.
   *
   * @var string
   */
  protected $framework;

  /**
   * The options.
   *
   * @var array
   */
  protected $options;

  /**
   * The active settings.
   *
   * @var array
   */
  protected $settings;

  /**
   * The CSS framework settings.
   *
   * @var array
   */
  protected $nestedSettings;

  /**
   * The jsonified config for js-driven layouts.
   *
   * @var string
   */
  protected $jsConfig;

  /**
   * The jsonified config for css-driven layouts.
   *
   * @var string
   */
  protected $cssConfig;

  /**
   * The required settings by admin preview.
   *
   * @var array
   */
  protected $jsSettings;

  /**
   * The main grids.
   *
   * @var array
   */
  protected $grids;

  /**
   * The main grids.
   *
   * @var array
   */
  protected $nestedGrids;

  /**
   * Which breakpoint to generate icon from.
   *
   * Foundation or Bootstrap 3 has `lg` for the largest.
   * Bootstrap 4 and all js|css-driven has `xl` for the largest.
   *
   * @var string
   */
  protected $iconBreakpoint = 'xl';

  /**
   * The smallest breakpoint can be SM for Foundation, XS for the rest.
   *
   * @var string
   */
  protected $smallestBreakpoint = 'xs';

  /**
   * The total breakpoint count.
   *
   * @var int
   */
  protected $breakpointCount = 0;

  /**
   * The layout engine.
   *
   * @var \Drupal\gridstack\GridStackEnginePluginInterface
   */
  protected $engine;

  /**
   * The region suggestions.
   *
   * @var array
   */
  protected $regionSuggestions;

  /**
   * The flag whether to use HTML5 autocomplete suggestions, or not.
   *
   * @var bool
   */
  protected $html5Ac = FALSE;

  /**
   * Defines the variant.
   *
   * @var string
   */
  protected $isVariant = FALSE;

  /**
   * Defines the nice name.
   *
   * @var string
   */
  protected $niceName = 'GridStack';

  /**
   * Defines the machine name.
   *
   * @var string
   */
  protected $machineName = 'gridstack';

  /**
   * The gridstack instance to distinguish from the current entity.
   *
   * @var Drupal\gridstack\Entity\GridStack
   */
  protected $gridStack;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    $instance = parent::create($container);
    $instance->fileSystem = $container
      ->get('file_system');
    $instance->blazyAdmin = $container
      ->get('blazy.admin');
    $instance->manager = $container
      ->get('gridstack.manager');
    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    $form = parent::form($form, $form_state);

    // Change page title for the duplicate operation.
    $label = $this->isVariant ? $this
      ->gridStack()
      ->label() : $this->entity
      ->label();
    if ($this->operation == 'duplicate') {
      $form['#title'] = $this
        ->t('<em>Duplicate gridstack optionset</em>: @label', [
        '@label' => $label,
      ]);
      $this->entity = $this->entity
        ->createDuplicate();
    }

    // Change page title for the edit operation.
    if ($this->operation == 'edit') {
      $form['#title'] = $this
        ->t('<em>Edit gridstack optionset</em>: @label', [
        '@label' => $this->entity
          ->label(),
      ]);
    }
    $classes = [
      'gridstack',
      'slick',
      'optionset',
      'gridstack--ui',
    ];
    $tooltip = [
      'class' => [
        'form-item--tooltip-bottom',
      ],
    ];
    $this->default = GridStack::load('default');
    $this->isDefault = $this
      ->gridStack()
      ->id() == 'default';
    $this->adminCss = $this->manager
      ->configLoad('admin_css', 'blazy.settings');
    $this->framework = $this->manager
      ->configLoad('framework', 'gridstack.settings');
    $this->useNested = $this->framework && $this
      ->useFramework();
    $this->options = $this->entity
      ->getOptions();
    $this->settings = $settings = $this
      ->gridStack()
      ->getOptions('settings') ?: [];
    $this->jsConfig = $this
      ->jsonify($settings, TRUE);
    $this->cssConfig = $this
      ->jsonify($this
      ->getNestedSettings(), TRUE);
    $this->grids = $this->entity
      ->getLastBreakpoint();
    $this->nestedGrids = $this->entity
      ->getLastBreakpoint('nested');
    $this->html5Ac = $this->manager
      ->configLoad('html5_ac', 'gridstack.settings');
    $js_settings = [
      'breakpoint' => 'lg',
      'optionset' => $this->entity
        ->isNew() ? 'default' : $this->entity
        ->id(),
    ];
    $this->settings['root'] = TRUE;
    $this->settings['display'] = 'main';
    $this->settings['storage'] = '';
    $this->settings['use_framework'] = $this->useNested;
    $this->jsSettings = array_merge($js_settings, $this->settings);

    // Initializes the layout engine.
    $this
      ->initEngine($form);
    $form['#attributes']['class'][] = 'is-gs-nojs';
    $form['#attributes']['class'][] = 'has-tooltip';
    $form['#attributes']['data-icon'] = $this->iconBreakpoint;
    $form['#attributes']['data-gs-html5-ac'] = empty($this->html5Ac) ? 0 : 1;
    foreach ($classes as $class) {
      $form['#attributes']['class'][] = 'form--' . $class;
    }
    if (!$this->entity
      ->isNew()) {
      $form['#attributes']['class'][] = 'form--optionset--' . str_replace('_', '-', $this
        ->gridStack()
        ->id());
    }
    if ($this->adminCss) {
      $form['#attached']['library'][] = 'blazy/admin';
      $form['#attributes']['class'][] = 'form--blazy-on';
    }
    else {
      $form['#attributes']['class'][] = 'form--blazy-off';
    }
    $base_settings = $this->default
      ->getOptions('settings');
    $form['#attached']['library'][] = 'gridstack/admin';
    $form['#attached']['drupalSettings']['gridstack'] = $base_settings;

    // Load all grids to get live preview going, except 12.
    // The 12 column is split into gridstack.library.css + gridstack.static.css.
    foreach (range(1, 11) as $key) {
      $form['#attached']['library'][] = 'gridstack/gridstack.' . $key;
    }
    $form['label'] = [
      '#type' => 'textfield',
      '#title' => $this
        ->t('Label'),
      '#default_value' => $this->entity
        ->label(),
      '#maxlength' => 255,
      '#required' => TRUE,
      '#description' => $this
        ->t("Label for the GridStack optionset."),
      '#wrapper_attributes' => $tooltip,
      '#prefix' => '<div class="form__header form__half form__half--first has-tooltip clearfix">',
    ];

    // Keep the legacy CTools ID, i.e.: name as ID.
    $form['name'] = [
      '#type' => 'machine_name',
      '#default_value' => $this->entity
        ->id(),
      '#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
      '#wrapper_attributes' => $tooltip,
      '#disabled' => !$this->entity
        ->isNew(),
      '#suffix' => '</div>',
      '#machine_name' => [
        'source' => [
          'label',
        ],
        'exists' => '\\Drupal\\gridstack\\Entity\\GridStack::load',
      ],
    ];
    $form['description'] = [
      '#type' => 'textarea',
      '#title' => $this
        ->t('Description'),
      '#default_value' => $this->entity
        ->description(),
      '#description' => $this
        ->t("Administrative description."),
      '#wrapper_attributes' => $tooltip,
    ];
    $form['screenshot'] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => [
          'form--gridstack__screenshot',
        ],
        'id' => 'gridstack-screenshot',
      ],
      '#weight' => 100,
    ];
    $form['canvas'] = [
      '#markup' => '<canvas id="gridstack-canvas"></canvas>',
      '#allowed_tags' => [
        'canvas',
      ],
      '#weight' => 100,
    ];
    $this
      ->jsonForm($form);
    return $form;
  }

  /**
   * Returns the gridstack instance.
   */
  protected function gridStack() {
    if (!isset($this->gridStack)) {
      $this->gridStack = $this->entity;
    }
    return $this->gridStack;
  }

  /**
   * Sets the gridstack instance.
   */
  protected function setGridStack(GridStack $gridstack) {
    $this->gridStack = $gridstack;
    return $this;
  }

  /**
   * Returns TRUE if should use framework.
   */
  protected function useFramework() {
    return $this
      ->gridStack()
      ->getOption('use_framework', FALSE);
  }

  /**
   * Initializes the layout engine.
   */
  protected function initEngine(array &$form) {
    if ($this->useNested) {
      $engine = $this->framework;
      $form['#attributes']['class'][] = 'form--framework is-framework';
      $form['#attributes']['class'][] = 'form--' . $this->framework;
      $this->settings = $this
        ->getNestedSettings();
    }
    else {

      // Always use default js-driven for the admin preview.
      $engine = 'gridstack_js';
      $form['#attributes']['class'][] = 'form--gridstack-js';
    }
    $this->engine = $this->manager
      ->engineManager()
      ->load($engine);
    $this->engine
      ->setOptionset($this
      ->gridStack());
    $this->iconBreakpoint = $this->engine
      ->getIconBreakpoint();
    $breakpoints = $this
      ->getApplicableBreakpoints();
    $this->breakpointCount = count($breakpoints);
    $this->smallestBreakpoint = $this->engine
      ->getSmallestBreakpoint();
    $form['#attributes']['class'][] = 'form--' . $this->engine
      ->get('group');
    $form['#attributes']['data-gs-breakpoint-count'] = $this->breakpointCount;
    $form['#attributes']['data-gs-smallest'][] = $this->smallestBreakpoint;
    $this
      ->regionForm($form);
  }

  /**
   * Returns the json form.
   */
  protected function jsonForm(array &$form) {
    $form['json'] = [
      '#type' => 'container',
      '#tree' => TRUE,
      '#attributes' => [
        'class' => [
          'gridstack-json',
          'visually-hidden',
        ],
      ],
      '#weight' => 100,
    ];
    $form['json']['breakpoints'] = [
      '#type' => 'hidden',
      '#default_value' => $this
        ->gridStack()
        ->getJson('breakpoints'),
    ];
    $form['json']['settings'] = [
      '#type' => 'hidden',
      '#default_value' => $this
        ->gridStack()
        ->getJson('settings'),
    ];
  }

  /**
   * Returns the region form.
   */
  protected function regionForm(array &$form) {
    $template_options = '';
    $attributes = [
      'id' => 'gridstack-regions',
    ];
    $attributes['class'][] = 'visually-hidden';
    if (empty($this->html5Ac)) {

      // @todo replace with core Awesomplete post 8.9+, see #3076171.
      $form['#attached']['library'][] = 'core/jquery.ui.autocomplete';
      $attributes['data-gs-regions'] = Json::encode(array_keys($this
        ->getRegionSuggestions()));
    }
    else {
      $options = [];
      foreach (array_keys($this
        ->getRegionSuggestions()) as $region) {
        $options[] = '<option>' . $region . '</option>';
      }
      $template_options = implode('', $options);
    }
    $form['regions'] = [
      '#markup' => '<template' . new Attribute($attributes) . '>' . $template_options . '</template>',
      '#allowed_tags' => [
        'template',
        'option',
      ],
      '#weight' => 100,
    ];
  }

  /**
   * Returns the region suggestions.
   */
  protected function getRegionSuggestions() {
    if (!isset($this->regionSuggestions)) {
      $cid = 'gridstack_region_suggestions';
      if ($cache = $this->manager
        ->getCache()
        ->get($cid)) {
        $this->regionSuggestions = $cache->data;
      }
      else {
        $positions = [
          'ads',
          'aside',
          'content',
          'featured',
          'footer',
          'header',
          'hightlight',
          'hotdamn',
          'main',
          'meta',
          'preface',
          'postscript',
          'sidebar',
          'spotlight',
          'widget',
        ];
        $sequences = [
          'first',
          'second',
          'third',
          'fourth',
          'fifth',
          'last',
        ];
        $edges = [
          'top',
          'middle',
          'bottom',
        ];
        $sub_positions = [
          'top_first',
          'top_second',
          'top_third',
          'top_fourth',
          'top_fifth',
          'top_last',
          'middle_first',
          'middle_second',
          'middle_third',
          'middle_fourth',
          'middle_fifth',
          'middle_last',
          'bottom_first',
          'bottom_second',
          'bottom_third',
          'bottom_fourth',
          'bottom_fifth',
          'bottom_last',
        ];
        $lasts = [
          'last_first',
          'last_second',
          'last_third',
          'last_fourth',
          'last_fifth',
          'last_last',
        ];
        $contents = [
          'bg',
          'carousel',
          'chart',
          'currency',
          'donation',
          'news',
          'slideshow',
          'time',
          'weather',
        ];
        $minimals = [
          'overlay',
        ];
        $regions = [];
        foreach ($positions as $region) {
          $regions[$region] = $region;
          foreach ($sequences as $position) {
            $regions[$region . '_' . $position] = $region . '_' . $position;
          }
          foreach ($edges as $position) {
            $regions[$region . '_' . $position] = $region . '_' . $position;
          }
          foreach ($sub_positions as $position) {
            $regions[$region . '_' . $position] = $region . '_' . $position;
          }
          foreach ($lasts as $position) {
            $regions[$region . '_' . $position] = $region . '_' . $position;
          }
        }
        foreach ($contents as $content) {
          $regions[$content] = $content;
        }
        foreach ($minimals as $region) {
          $regions[$region] = $region;
          foreach ($sequences as $position) {
            $regions[$region . '_' . $position] = $region . '_' . $position;
          }
          foreach ($edges as $position) {
            $regions[$region . '_' . $position] = $region . '_' . $position;
          }
        }
        $this->manager
          ->getModuleHandler()
          ->alter('gridstack_region_suggestions', $regions);
        $count = count($regions);
        $tags = Cache::buildTags($cid, [
          'count:' . $count,
        ]);
        $this->manager
          ->getCache()
          ->set($cid, $regions, Cache::PERMANENT, $tags);
        $this->regionSuggestions = $regions;
      }
    }
    return $this->regionSuggestions;
  }

  /**
   * Returns the CSS settings.
   */
  protected function getNestedSettings() {
    if (!isset($this->nestedSettings)) {
      $settings = $this->default
        ->getOptions('settings');
      $framework['minWidth'] = 1;
      $framework['verticalMargin'] = 20;
      $framework['column'] = 12;
      $framework['disableOneColumnMode'] = TRUE;
      $this->nestedSettings = array_merge($settings, $framework);
    }
    return $this->nestedSettings;
  }

  /**
   * Returns the supported columns.
   */
  protected function getColumnOptions() {
    $range = range(1, 12);
    return array_combine($range, $range);
  }

  /**
   * Returns JSON for options.breakpoints[xs|sm|md|lg|xl] keyed by nodes.
   *
   * Revert back from keys to keys and values:
   * Original: [[1,0,2,8].
   * Now: [{"x":1,"y":0,"width":2,"height":8}.
   */
  protected function getNodes($grids = '', $exclude_region = FALSE, $stringify = TRUE) {
    if ($grids) {
      $grids = is_string($grids) ? Json::decode($grids) : $grids;
      $values = [];
      foreach (array_values($grids) as $grid) {
        $value = $this->entity
          ->getNode($grid, $exclude_region);
        $values[] = $value ? (object) $value : [];
      }
      return $stringify ? Json::encode($values) : $values;
    }
    return '';
  }

  /**
   * Returns JSON for options.breakpoints[xs|sm|md|lg|xl] keyed by nodes.
   *
   * Revert back from keys to keys and values:
   * Original: [[1,0,2,8].
   * Now: [{"x":1,"y":0,"width":2,"height":8}.
   */
  protected function getNodesNested($grids = '', $nested_grids = '', $exclude_region = FALSE) {
    if ($grids && $nested_grids) {
      $grids = is_string($grids) ? Json::decode($grids) : $grids;
      $nested_grids = is_string($nested_grids) ? Json::decode($nested_grids) : $nested_grids;
      $nested_grids = array_values($nested_grids);
      $values = [];
      foreach (array_keys($grids) as $id) {
        if (isset($nested_grids[$id])) {
          if (empty($nested_grids[$id])) {
            $values[] = [];
          }
          else {
            $values[] = $this
              ->getNodes($nested_grids[$id], $exclude_region, FALSE);
          }
        }
      }
      return Json::encode($values);
    }
    return '';
  }

  /**
   * Returns the applicable breakpoints.
   */
  protected function getApplicableBreakpoints() {
    $engine = isset($this->engine) ? $this->engine : NULL;
    $breakpoints = $engine ? $engine
      ->sizes() : GridStackDefault::breakpoints();
    $breakpoints = array_keys($breakpoints);

    // Only provides one breakpoint for default.
    if ($this
      ->gridStack()
      ->id() == 'default') {
      $breakpoints = [
        'lg',
      ];
    }
    return $breakpoints;
  }

  /**
   * Massages the settings specific for when CSS Framework is disabled.
   */
  protected function massageSettings(array &$form) {
    $excludes = [
      'container',
      'details',
      'item',
      'hidden',
      'submit',
    ];
    foreach ($this->default
      ->getOptions('settings') as $name => $value) {
      if (!isset($form['options']['settings'][$name])) {
        continue;
      }
      if (in_array($form['options']['settings'][$name]['#type'], $excludes) && !isset($form['options']['settings'][$name])) {
        continue;
      }
      if ($this->adminCss) {
        if ($form['options']['settings'][$name]['#type'] == 'checkbox') {
          $form['options']['settings'][$name]['#field_suffix'] = '&nbsp;';
          $form['options']['settings'][$name]['#title_display'] = 'before';
        }
      }
      if (!isset($form['options']['settings'][$name]['#default_value'])) {
        $form['options']['settings'][$name]['#default_value'] = isset($this->settings[$name]) ? $this->settings[$name] : $value;
      }
    }
  }

  /**
   * Returns user input values.
   */
  protected static function getUserInputValues(array $element, FormStateInterface $form_state) {

    // Default to using the current selection if the form is new.
    $path = isset($element['#parents']) ? $element['#parents'] : [];

    // We need to use the actual user input, since when #limit_validation_errors
    // is used, the unvalidated user input is not added to the form state.
    // @see FormValidator::handleErrorsWithLimitedValidation()
    return NestedArray::getValue($form_state
      ->getUserInput(), $path);
  }

  /**
   * Convert the config into a JSON object to reduce logic at frontend.
   */
  protected function jsonify(array $options = [], $preview = FALSE) {
    if (empty($options)) {
      return '';
    }
    $json = [];
    $default = GridStack::load('default')
      ->getOptions('settings');
    $cellHeight = $options['cellHeight'];
    $excludes = [
      'auto',
      'column',
      'float',
      'rtl',
      'minWidth',
      // @todo remove post gridstack_update_8214.
      // @todo 'resizable',
      'disableResize',
      'staticGrid',
      'draggable',
      'disableDrag',
    ];
    if (isset($options['column']) && $options['column'] == 12) {
      unset($options['column']);
    }
    foreach ($options as $name => $value) {

      // @todo unset($options['noMargin']);
      if (!in_array($name, [
        'cellHeight',
        'rtl',
      ]) && isset($default[$name])) {
        $cast = gettype($default[$name]);
        settype($options[$name], $cast);
      }
      $json[$name] = $options[$name];
      $json['cellHeight'] = $cellHeight == -1 ? 'auto' : (int) $cellHeight;
      if (empty($options['rtl'])) {
        unset($json['rtl']);
      }

      // Be sure frontend options do not break admin preview.
      if ($preview && in_array($name, $excludes)) {
        unset($json[$name]);
      }
    }
    if ($preview) {

      // Do not set resizable here, will do it at JS with array options.
      $json['cellHeight'] = $cellHeight == -1 ? 60 : (int) $cellHeight;
    }
    return Json::encode($json);
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);

    // Grids contain the current grid node and probably nested grids.
    $framework = $form_state
      ->getValue([
      'options',
      'use_framework',
    ]);

    // @todo remove after a hook_update.
    $form_state
      ->unsetValue([
      'options',
      'type',
    ]);
    if (!$form_state
      ->hasValue([
      'json',
      'grids',
      'nested',
    ])) {
      $form_state
        ->unsetValue([
        'json',
        'grids',
        'nested',
      ]);
    }

    // Columns.
    $settings = $form_state
      ->getValue([
      'options',
      'settings',
    ]);
    $options_breakpoints = $form_state
      ->getValue([
      'options',
      'breakpoints',
    ]);

    // Validate breakpoint form.
    if (!empty($options_breakpoints)) {
      $this
        ->validateBreakpointForm($form, $form_state);
    }

    // Remove JS settings for static grid layout like Bootstrap/ Foundation.
    if (!empty($framework)) {
      $settings = [];
      $form_state
        ->setValue([
        'options',
        'settings',
      ], []);
    }

    // Map settings into JSON.
    $form_state
      ->setValue([
      'json',
      'settings',
    ], empty($settings) ? '' : $this
      ->jsonify($settings));

    // JS only breakpoints.
    // Only reasonable for GridStack, not Bootstrap, or other static grid.
    // JSON breakpoints to reduce frontend logic for responsive JS.
    $json_breakpoints = [];
    if (!empty($options_breakpoints)) {
      foreach ($options_breakpoints as $breakpoints) {

        // Makes it possible to have 3 out of 5 breakpoints like BS3/Foundation.
        if (empty($breakpoints['width'])) {
          continue;
        }
        if (!empty($breakpoints['column'])) {
          $json_breakpoints[$breakpoints['width']] = empty($framework) ? (int) $breakpoints['column'] : 12;
        }
      }
    }

    // Append the desktop version as well to reduce JS logic.
    $form_state
      ->setValue([
      'json',
      'breakpoints',
    ], empty($json_breakpoints) ? '' : Json::encode($json_breakpoints));

    // Build icon.
    if ($icon = $form_state
      ->getValue([
      'options',
      'icon',
    ])) {
      $id = $form_state
        ->getValue('name');
      if (strpos($icon, 'data:image') !== FALSE) {
        $destination = 'public://gridstack';
        $paths['id'] = $id;
        $paths['target'] = $destination . '/';
        $this->fileSystem
          ->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY);
        $this
          ->saveImage($icon, $paths);

        // Update data URI into file URI.
        if (!empty($paths['uri'])) {
          if (strpos($paths['uri'], 'data:,') !== FALSE) {
            $paths['uri'] = '';
          }
          $form_state
            ->setValue([
            'options',
            'icon',
          ], $paths['uri']);
        }
      }
    }
  }

  /**
   * Validate breakpoint form.
   */
  protected function validateBreakpointForm(array &$form, FormStateInterface &$form_state) {
    $options_breakpoints = $form_state
      ->getValue([
      'options',
      'breakpoints',
    ]);
    $framework = $form_state
      ->getValue([
      'options',
      'use_framework',
    ]);
    foreach ($options_breakpoints as $key => $breakpoints) {
      foreach ($breakpoints as $k => $value) {

        // Static grids only expect 12 columns, not dynamic ones.
        if (!empty($framework)) {
          $breakpoints['column'] = 12;
          if ($k == 'column') {
            $value = 12;
          }
        }

        // Respect 0 value for future mobile first when Blazy supports it.
        if (isset($breakpoints['column'])) {
          $form_state
            ->setValue([
            'options',
            'breakpoints',
            $key,
            $k,
          ], $value);
        }
      }

      // Remove breakpoint grids if no width provided.
      // Makes it possible to have 3 out of 5 breakpoints like BS3/Foundation.
      if (empty($breakpoints['width'])) {
        $form_state
          ->unsetValue([
          'options',
          'breakpoints',
          $key,
        ]);
      }

      // @todo remove $form_state->unsetValue(['options', 'breakpoints', $key, 'revert']);
      // Clean out stuffs, either stored somewhere else, or no use.
      $nested = $form_state
        ->getValue([
        'options',
        'breakpoints',
        $key,
        'nested',
      ]);
      $nested_all = Json::decode($nested);
      $nested = empty($nested_all) ? '' : array_filter($nested_all);
      $grids = $form_state
        ->getValue([
        'options',
        'breakpoints',
        $key,
        'grids',
      ]);
      $grids_all = Json::decode($grids);
      $grids = empty($grids_all) ? '' : array_filter($grids_all);
      if (empty($nested) || empty($grids)) {
        $form_state
          ->unsetValue([
          'options',
          'breakpoints',
          $key,
          'nested',
        ]);
      }

      // Simplify storage to just array without keys like at frontend.
      // @todo put this into the loop above.
      if ($grids) {
        $exclude_region = $key != $this->entity
          ->getLastBreakpointKey();
        $main_grids = $this->entity
          ->getJsonSummaryBreakpoints($key, $grids_all, $exclude_region);
        $form_state
          ->setValue([
          'options',
          'breakpoints',
          $key,
          'grids',
        ], $main_grids);
        if ($nested) {
          $nested_grids = $this->entity
            ->getJsonSummaryNestedBreakpoints($key, $nested_all);
          $form_state
            ->setValue([
            'options',
            'breakpoints',
            $key,
            'nested',
          ], $nested_grids);
        }
      }

      // Remove useless breakpoint key.
      $form_state
        ->unsetValue([
        'options',
        'breakpoints',
        $key,
        'breakpoint',
      ]);
    }
  }

  /**
   * Defines breakpoints form adopted from Blazy deprecated methods.
   *
   * We add custom classes to support various admin themes where applicable.
   */
  protected function breakpointElements() {
    $form = [];
    foreach ($this
      ->getApplicableBreakpoints() as $breakpoint) {
      $form[$breakpoint]['breakpoint'] = [
        '#type' => 'markup',
        '#markup' => $breakpoint,
        '#weight' => 1,
        '#wrapper_attributes' => [
          'class' => [
            'form-item--right',
          ],
        ],
        '#access' => !$this->useNested,
      ];
      $form[$breakpoint]['width'] = [
        '#type' => 'textfield',
        '#title' => $this
          ->t('Width'),
        '#title_display' => 'invisible',
        '#description' => $this
          ->t('See <strong>XS</strong> for detailed info.'),
        '#max_length' => 32,
        '#size' => 6,
        '#weight' => 3,
        '#attributes' => [
          'class' => [
            'form-text--width',
          ],
        ],
        '#wrapper_attributes' => [
          'class' => [
            'form-item--width',
          ],
        ],
        '#disabled' => $this->isVariant,
      ];
    }
    return $form;
  }

  /**
   * Saves the icon based on the current grid display.
   *
   * Taken and simplified from color.module _color_render_images(), and s.o.
   */
  protected function saveImage($data, array &$paths) {
    if (empty($data) || strpos($data, ',') === FALSE) {
      return;
    }
    $name = $paths['id'] . '.png';
    $uri = $paths['target'] . $name;
    $url = file_create_url($uri);
    $real_path = $this->fileSystem
      ->realpath($uri);

    // Remove "data:image/png;base64," part.
    $file_data = substr($data, strpos($data, ',') + 1);
    $file_contents = base64_decode($file_data);
    if (empty($file_contents)) {
      return;
    }
    $image = imagecreatefromstring($file_contents);

    // Gets dimensions.
    $width = imagesx($image);
    $height = imagesy($image);

    // Prepare target buffer.
    $target = imagecreatetruecolor($width, $height);
    $white = imagecolorallocate($target, 255, 255, 255);
    imagefilledrectangle($target, 0, 0, $width, $height, $white);
    imagecopy($target, $image, 0, 0, 0, 0, $width, $height);
    imagealphablending($target, TRUE);
    imagepng($target, $real_path);

    // Clean up target buffer.
    imagedestroy($target);

    // Store image.
    $paths['uri'] = $uri;
    $paths['url'] = file_url_transform_relative($url);
    $this->fileSystem
      ->saveData($file_contents, $uri, FileSystemInterface::EXISTS_REPLACE);

    // Set standard file permissions for webserver-generated files.
    $this->fileSystem
      ->chmod($real_path);
  }

  /**
   * Overrides Drupal\Core\Entity\EntityFormController::save().
   */
  public function save(array $form, FormStateInterface $form_state) {
    parent::save($form, $form_state);
    $entity = $this->entity;

    // Prevent leading and trailing spaces in gridstack names.
    $entity
      ->set('label', Html::escape(trim($entity
      ->label())));
    $entity
      ->set('id', $entity
      ->id());
    $entity
      ->set('description', strip_tags($entity
      ->description()));
    $enable = $entity
      ->id() == 'default' ? FALSE : TRUE;
    $entity
      ->setStatus($enable);
    $status = $entity
      ->save();
    $label = $entity
      ->label();
    $edit_link = $entity
      ->toLink($this
      ->t('Edit'), 'edit-form')
      ->toString();
    $config_prefix = $entity
      ->getEntityType()
      ->getConfigPrefix();
    $message = [
      '@config_prefix' => $config_prefix,
      '%label' => $label,
    ];
    $notice = [
      '@config_prefix' => $config_prefix,
      '%label' => $label,
      'link' => $edit_link,
    ];
    if ($status == SAVED_UPDATED) {

      // If we edited an existing entity.
      // @todo #2278383.
      $this
        ->messenger()
        ->addMessage($this
        ->t('@config_prefix %label has been updated.', $message));
      $this
        ->logger($this->machineName)
        ->notice('@config_prefix %label has been updated.', $notice);
    }
    else {

      // If we created a new entity.
      $this
        ->messenger()
        ->addMessage($this
        ->t('@config_prefix %label has been added.', $message));
      $this
        ->logger($this->machineName)
        ->notice('@config_prefix %label has been added.', $notice);
    }
    $form_state
      ->setRedirectUrl($entity
      ->toUrl('collection'));
  }

}

Classes

Namesort descending Description
GridStackFormBase Extends base form for gridstack instance configuration form.