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;
class GridStackFormBase extends EntityForm {
protected $fileSystem;
protected $blazyAdmin;
protected $manager;
protected $default;
protected $adminCss;
protected $isDefault;
protected $useNested = FALSE;
protected $framework;
protected $options;
protected $settings;
protected $nestedSettings;
protected $jsConfig;
protected $cssConfig;
protected $jsSettings;
protected $grids;
protected $nestedGrids;
protected $iconBreakpoint = 'xl';
protected $smallestBreakpoint = 'xs';
protected $breakpointCount = 0;
protected $engine;
protected $regionSuggestions;
protected $html5Ac = FALSE;
protected $isVariant = FALSE;
protected $niceName = 'GridStack';
protected $machineName = 'gridstack';
protected $gridStack;
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;
}
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$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();
}
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);
$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;
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">',
];
$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;
}
protected function gridStack() {
if (!isset($this->gridStack)) {
$this->gridStack = $this->entity;
}
return $this->gridStack;
}
protected function setGridStack(GridStack $gridstack) {
$this->gridStack = $gridstack;
return $this;
}
protected function useFramework() {
return $this
->gridStack()
->getOption('use_framework', FALSE);
}
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 {
$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);
}
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'),
];
}
protected function regionForm(array &$form) {
$template_options = '';
$attributes = [
'id' => 'gridstack-regions',
];
$attributes['class'][] = 'visually-hidden';
if (empty($this->html5Ac)) {
$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,
];
}
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;
}
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;
}
protected function getColumnOptions() {
$range = range(1, 12);
return array_combine($range, $range);
}
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 '';
}
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 '';
}
protected function getApplicableBreakpoints() {
$engine = isset($this->engine) ? $this->engine : NULL;
$breakpoints = $engine ? $engine
->sizes() : GridStackDefault::breakpoints();
$breakpoints = array_keys($breakpoints);
if ($this
->gridStack()
->id() == 'default') {
$breakpoints = [
'lg',
];
}
return $breakpoints;
}
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'] = ' ';
$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;
}
}
}
protected static function getUserInputValues(array $element, FormStateInterface $form_state) {
$path = isset($element['#parents']) ? $element['#parents'] : [];
return NestedArray::getValue($form_state
->getUserInput(), $path);
}
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',
'disableResize',
'staticGrid',
'draggable',
'disableDrag',
];
if (isset($options['column']) && $options['column'] == 12) {
unset($options['column']);
}
foreach ($options as $name => $value) {
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']);
}
if ($preview && in_array($name, $excludes)) {
unset($json[$name]);
}
}
if ($preview) {
$json['cellHeight'] = $cellHeight == -1 ? 60 : (int) $cellHeight;
}
return Json::encode($json);
}
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$framework = $form_state
->getValue([
'options',
'use_framework',
]);
$form_state
->unsetValue([
'options',
'type',
]);
if (!$form_state
->hasValue([
'json',
'grids',
'nested',
])) {
$form_state
->unsetValue([
'json',
'grids',
'nested',
]);
}
$settings = $form_state
->getValue([
'options',
'settings',
]);
$options_breakpoints = $form_state
->getValue([
'options',
'breakpoints',
]);
if (!empty($options_breakpoints)) {
$this
->validateBreakpointForm($form, $form_state);
}
if (!empty($framework)) {
$settings = [];
$form_state
->setValue([
'options',
'settings',
], []);
}
$form_state
->setValue([
'json',
'settings',
], empty($settings) ? '' : $this
->jsonify($settings));
$json_breakpoints = [];
if (!empty($options_breakpoints)) {
foreach ($options_breakpoints as $breakpoints) {
if (empty($breakpoints['width'])) {
continue;
}
if (!empty($breakpoints['column'])) {
$json_breakpoints[$breakpoints['width']] = empty($framework) ? (int) $breakpoints['column'] : 12;
}
}
}
$form_state
->setValue([
'json',
'breakpoints',
], empty($json_breakpoints) ? '' : Json::encode($json_breakpoints));
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);
if (!empty($paths['uri'])) {
if (strpos($paths['uri'], 'data:,') !== FALSE) {
$paths['uri'] = '';
}
$form_state
->setValue([
'options',
'icon',
], $paths['uri']);
}
}
}
}
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) {
if (!empty($framework)) {
$breakpoints['column'] = 12;
if ($k == 'column') {
$value = 12;
}
}
if (isset($breakpoints['column'])) {
$form_state
->setValue([
'options',
'breakpoints',
$key,
$k,
], $value);
}
}
if (empty($breakpoints['width'])) {
$form_state
->unsetValue([
'options',
'breakpoints',
$key,
]);
}
$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',
]);
}
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);
}
}
$form_state
->unsetValue([
'options',
'breakpoints',
$key,
'breakpoint',
]);
}
}
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;
}
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);
$file_data = substr($data, strpos($data, ',') + 1);
$file_contents = base64_decode($file_data);
if (empty($file_contents)) {
return;
}
$image = imagecreatefromstring($file_contents);
$width = imagesx($image);
$height = imagesy($image);
$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);
imagedestroy($target);
$paths['uri'] = $uri;
$paths['url'] = file_url_transform_relative($url);
$this->fileSystem
->saveData($file_contents, $uri, FileSystemInterface::EXISTS_REPLACE);
$this->fileSystem
->chmod($real_path);
}
public function save(array $form, FormStateInterface $form_state) {
parent::save($form, $form_state);
$entity = $this->entity;
$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) {
$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 {
$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'));
}
}