You are here

GridStackEnginePluginBase.php in GridStack 8.2

Namespace

Drupal\gridstack

File

src/GridStackEnginePluginBase.php
View source
<?php

namespace Drupal\gridstack;

use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\NestedArray;
use Drupal\blazy\Blazy;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides base class for all gridstack layout engines.
 */
abstract class GridStackEnginePluginBase extends GridStackPluginBase implements GridStackEnginePluginInterface {

  /**
   * The prefix class dependent on framework/ versions: col, cell, columns, etc.
   *
   * @var array
   */
  protected $colClass = 'col';

  /**
   * The last prefix dependent on framework/ versions: col-, large, etc.
   *
   * @var array
   */
  protected $colPrefix = 'col-';

  /**
   * The layout sizes.
   *
   * @var array
   */
  protected $sizes = [];

  /**
   * The layout CSS classes for options.
   *
   * @var array
   */
  protected $classOptions = [];

  /**
   * The layout CSS row classes for options.
   *
   * @var array
   */
  protected $rowClassOptions = [];

  /**
   * The container classes, actually refers to row classes, not the outmost.
   *
   * @var array
   */
  protected $containerClasses = [];

  /**
   * The nested container classes.
   *
   * @var array
   */
  protected $nestedContainerClasses = [];

  /**
   * The item classes, .box.
   *
   * @var array
   */
  protected $itemClasses = [];

  /**
   * The item content classes, .box__content.
   *
   * @var array
   */
  protected $itemContentClasses = [];

  /**
   * The admin regions.
   *
   * @var array
   */
  protected $regions = [];

  /**
   * The above-fold CSS inline styles as recommended by lighthouse.
   *
   * @var array
   */
  protected $styles = [];

  /**
   * The stylizer service.
   *
   * @var \Drupal\gridstack\GridStackStylizerInterface
   */
  protected $stylizer;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $instance->stylizer = $container
      ->get('gridstack.stylizer');
    $instance->sizes = $instance
      ->setSizes(GridStackDefault::breakpoints());
    $instance->classOptions = $instance
      ->setClassOptions();
    $instance->rowClassOptions = $instance
      ->setRowClassOptions();
    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function classOptions() {
    return $this->classOptions;
  }

  /**
   * Sets the plugin engine classes for options, container or item, hard-coded.
   */
  protected function setClassOptions() {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function rowClassOptions() {
    return $this->rowClassOptions;
  }

  /**
   * Sets the optional plugin engine classes for options, row, hard-coded.
   */
  protected function setRowClassOptions() {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function containerClasses() {
    return $this->containerClasses;
  }

  /**
   * Sets the optional plugin engine container classes, configurable.
   */
  protected function setContainerClasses(array $classes = []) {
    $this->containerClasses = array_merge($this->containerClasses, $classes);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function sizes() {
    return $this->sizes;
  }

  /**
   * Sets the sizes.
   */
  protected function setSizes(array $sizes) {
    return $sizes;
  }

  /**
   * {@inheritdoc}
   */
  public function styles() {
    return $this->styles;
  }

  /**
   * Sets the styles, might be string, or array.
   */
  protected function setStyles(array $data) {
    $id = $this
      ->getPluginId() . '-' . $this
      ->getOptionset()
      ->id();
    $this->styles[Blazy::getHtmlId('gridstack', $id)][$data['context']] = $data['rules'];
    return $this;
  }

  /**
   * Return the icon 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.
   */
  public function getIconBreakpoint() {
    $keys = array_keys($this->sizes);
    return end($keys);
  }

  /**
   * Returns the smallest breakpoint, xs or sm.
   */
  public function getSmallestBreakpoint() {
    $keys = array_keys($this->sizes);
    return $keys[0];
  }

  /**
   * Provides gridstack skins and libraries.
   */
  public function attach(array &$load, array $attach = []) {
    if (!empty($attach['debug'])) {
      $load['library'][] = 'gridstack/debug';
    }
  }

  /**
   * {@inheritdoc}
   */
  public function build(array &$build, array &$element) {
    $settings =& $build['settings'];

    // Allows a layout variant to modify the established optionset.
    // Since render array output is cached, nothing to lose, just flexibility.
    if (!$this->optionset || !empty($settings['_variant'])) {
      $this
        ->setOptionset($build['optionset']);
    }

    // Defines stuffs early to be processed by ::buildItems.
    $this
      ->prepare($element, $settings);
    $element['#items'] = $this->stylizer
      ->getStyle('contentless', $settings) ? [] : $this
      ->buildItems($build);

    // Attached layout inline styles if so configured.
    // Runs after ::buildItems to aggregate styles.
    if ($styles = $this
      ->styles()) {
      $this->stylizer
        ->rootStyles($element, $styles, $settings);
    }
  }

  /**
   * Prepares the settings, selector and active styles.
   */
  private function prepare(array &$element, array &$settings) {
    $attributes = $content_attributes = [];
    $settings = array_merge($settings, $this->stylizer
      ->getStyle('all', $settings));
    $settings['_level'] = GridStackDefault::LEVEL_ROOT;
    $settings['rid'] = $settings['_context'] = GridStackDefault::ROOT;
    $selector = '.gridstack--' . str_replace('_', '-', $settings['optionset']);

    // Unique class per layout which can be many similar on a page.
    if (isset($settings['gid']) && ($gid = $settings['gid'])) {
      $selector .= '.is-gs-' . str_replace([
        '_',
        ':',
      ], '-', $gid);
    }

    // @todo $settings['_ungrid'] = empty($settings['_ipe']) && $this->stylizer->getStyle('parallax', $settings);
    if (!empty($this
      ->containerClasses()[0])) {
      $selector .= '.' . $this
        ->containerClasses()[0];
    }
    $settings['_selector'] = $selector;
    $this
      ->setConfiguration($settings);

    // Provides attributes and background media if so configured.
    // Runs before ::containerAttributes to pass the media settings.
    $this->stylizer
      ->prepare($element, $attributes, $settings, $this
      ->getOptionset());

    // Runs after ::media to process the media settings.
    $this
      ->containerAttributes($attributes, $settings);
    $this
      ->contentAttributes($content_attributes, $settings);

    // Pass attributes and items to templates.
    $element['#attributes'] = empty($element['#attributes']) ? $attributes : NestedArray::mergeDeep($element['#attributes'], $attributes);
    $element['#content_attributes'] = empty($element['#content_attributes']) ? $content_attributes : NestedArray::mergeDeep($element['#content_attributes'], $content_attributes);
    $element['#settings'] = empty($element['#settings']) ? $settings : NestedArray::mergeDeep($element['#settings'], $settings);
  }

  /**
   * {@inheritdoc}
   */
  public function buildItems(array $build) {
    $settings = $build['settings'];
    $settings = array_diff_key($settings, GridStackDefault::regionSettings());
    $grids = $this->optionset
      ->getLastBreakpoint();
    $items = [];
    $regions = $this->optionset
      ->prepareRegions(FALSE);

    // Cleans up top level settings.
    unset($settings['cache_metadata'], $settings['cache_tags'], $settings['_item']);
    foreach ($build['items'] as $delta => $item) {

      // Skips if more than we can chew, otherwise broken grid anyway.
      if (!isset($grids[$delta])) {
        continue;
      }
      $rid = GridStackDefault::regionId($delta);
      $region = isset($regions[$rid]) ? $regions[$rid] : [];
      $config = isset($item['settings']) ? array_merge($settings, $item['settings']) : $settings;
      $config['region'] = $region;
      $config['delta'] = $config['root_delta'] = $delta;
      $config['rid'] = $rid;

      // @todo move and refactor into region for consistent info back|front-end.
      foreach ([
        '_level',
        '_context',
        '_root',
      ] as $key) {
        $config[$key] = isset($region[$key]) ? $region[$key] : FALSE;
      }
      $items[] = $this
        ->buildItem($delta, $config, $item, $regions);
    }
    return $items;
  }

  /**
   * Provides nested items if so configured.
   */
  protected function buildNestedItems($delta, array &$settings, array $item, array $grids, array $regions = []) {
    $items = [];
    $index = $delta + 1;
    foreach (array_keys($grids) as $gid) {
      $rid = GridStackDefault::regionId($delta . '_' . $gid);
      $region = isset($regions[$rid]) ? $regions[$rid] : [];
      $nested = isset($item[$gid]) ? $item[$gid] : [];
      $config = isset($nested['settings']) ? array_merge($settings, $nested['settings']) : $settings;
      $config['region'] = $region;
      $config['rid'] = $rid;
      $config['nested_delta'] = $gid;
      $config['nested_id'] = $region['_context'];
      $config['use_inner'] = TRUE;

      // @todo move and refactor into region for consistent info back|front-end.
      foreach ([
        '_level',
        '_context',
        '_root',
      ] as $key) {
        $config[$key] = isset($region[$key]) ? $region[$key] : FALSE;
      }
      $items[] = $this
        ->buildItem($gid, $config, $nested, $regions);
    }

    // Provides nested gridstack, gridstack within gridstack, if so configured.
    $settings['_root'] = 'grids';
    $settings['_level'] = GridStackDefault::LEVEL_NESTED;
    $settings['_context'] = GridStackDefault::NESTED . $index;
    $settings['ungrid'] = !empty($settings['_ungrid']);

    // Update box with nested boxes.
    return [
      '#theme' => 'gridstack',
      '#items' => $items,
      '#optionset' => $this->optionset,
      '#settings' => $settings,
      '#attributes' => $this
        ->nestedContainerAttributes($settings),
    ];
  }

  /**
   * Modifies item content and attributes.
   */
  protected function modifyItem($delta, array &$settings, array &$content, array &$attributes, array &$content_attributes, array $regions = []) {
    $settings['contentless'] = $this->stylizer
      ->getStyle('contentless', $settings);
    $this
      ->itemAttributes($attributes, $settings);
    $this
      ->itemContentAttributes($content_attributes, $settings);

    // Provides nested items if so configured.
    if ($this
      ->getSetting('use_nested')) {
      $this
        ->modifyNestedItem($delta, $settings, $content, $attributes, $content_attributes, $regions);
    }

    // Allows stylizer to modify contents and attributes.
    if ($this
      ->getSetting('_stylizer')) {
      $this->stylizer
        ->modifyItem($delta, $settings, $content, $attributes, $content_attributes, $regions);
    }
  }

  /**
   * Modifies nested item contents and attributes.
   */
  protected function modifyNestedItem($delta, array &$settings, array &$content, array &$attributes, array &$content_attributes, array $regions = []) {

    // Provides configurable content attributes via Layout Builder.
    $this
      ->attributes($content_attributes, $settings);

    // Nested grids with preserved indices even if empty so to layout.
    // Only CSS Framework has nested grids for now, not js-driven layouts.
    if (isset($content['box'][0], $content['box'][0]['box']) && ($nesteds = $this->optionset
      ->getNestedGridsByDelta($delta))) {
      $settings['nested'] = TRUE;
      $settings['root'] = FALSE;
      $settings['use_inner'] = FALSE;

      // Overrides with a sub-theme_gridstack() containing nested boxes.
      $content['box'] = $this
        ->buildNestedItems($delta, $settings, $content['box'], $nesteds, $regions);
      $attributes['class'][] = 'box--nester';
    }
    if (isset($settings['_root']) && $settings['_root'] == 'nested') {
      $attributes['class'][] = 'box--nested';
    }
  }

  /**
   * Returns an individual item.
   */
  protected function buildItem($delta, array &$settings, array $item = [], array $regions = []) {
    $content_attributes = [];
    $attributes = isset($item['attributes']) ? $item['attributes'] : [];
    $content = [
      'box' => isset($item['box']) ? $item['box'] : [],
      'caption' => isset($item['caption']) ? $item['caption'] : [],
      'preface' => isset($item['preface']) ? $item['preface'] : [],
    ];
    if (empty($settings['_dummy'])) {
      $this
        ->modifyItem($delta, $settings, $content, $attributes, $content_attributes, $regions);
    }
    return [
      '#theme' => 'gridstack_box',
      '#item' => $content,
      '#delta' => $delta,
      '#attributes' => $attributes,
      '#content_attributes' => $content_attributes,
      '#settings' => $settings,
    ];
  }

  /**
   * Modifies the .box attributes.
   */
  protected function itemAttributes(array &$attributes, array &$settings) {
    $this
      ->setConfiguration($settings);
    $attributes['class'] = empty($attributes['class']) ? $this->itemClasses : array_merge($attributes['class'], $this->itemClasses);

    // Provides configurable item attributes.
    if ($this->stylizer
      ->getStyle('ete', $settings)) {
      $attributes['class'][] = 'box--ete';
    }
  }

  /**
   * Modifies the .box__content attributes.
   */
  protected function itemContentAttributes(array &$attributes, array &$settings) {
    $attributes['class'] = empty($attributes['class']) ? $this->itemContentClasses : array_merge($attributes['class'], $this->itemContentClasses);
  }

  /**
   * Returns the .gridstack container attributes.
   */
  public function containerAttributes(array &$attributes, array &$settings) {
    $attributes['class'] = empty($settings['_ungrid']) ? $this
      ->containerClasses() : [];

    // Provides configurable attributes via Layout Builder.
    $this
      ->attributes($attributes, $settings);

    // Add debug class for admin usages.
    if ($this
      ->getSetting('debug') || $this
      ->getSetting('_ipe')) {
      $attributes['class'][] = 'is-gs-debug';
      if ($this
        ->getSetting('_ipe')) {
        $attributes['class'][] = 'is-gs-lb';
        $attributes['data-region'] = GridStackDefault::ROOT;

        // Prevents parallax from messing around the admin page.
        // We'll toggle as needed via JS if doable instead.
        if (in_array('is-gs-parallax', $attributes['class'])) {
          $removed = [
            'is-gs-parallax',
            'is-gs-parallax-fs',
          ];
          $attributes['class'] = array_diff($attributes['class'], $removed);
        }
      }
      if ($this
        ->getSetting('_lbux')) {
        $attributes['class'][] = 'is-gs-lbux';
      }
    }

    // Empty it after being processed to not leak to children due to similarity.
    unset($settings['attributes'], $settings['wrapper_classes'], $settings['fw_classes']);

    // Unique class per layout which can be many similar on a page.
    if ($gid = $this
      ->getSetting('gid')) {
      $attributes['class'][] = 'is-gs-' . str_replace([
        '_',
        ':',
      ], '-', $gid);
    }

    // Unique class per layout variant which can be many similar on a page.
    if ($variant = $this
      ->getSetting('_variant')) {
      $attributes['class'][] = $this->stylizer
        ->style()
        ->getVariantClass($variant);
    }

    // Adds .blazy container to manage lazy load, or lightboxes per layout.
    Blazy::containerAttributes($attributes, $settings);

    // Provides configurable item attributes.
    if ($this->stylizer
      ->getStyle('parallax-fs', $settings)) {
      $attributes['class'][] = 'is-b-scroll';

      // @todo remove post blazy:2.1+.
      // Old bLazy, not IO, needs scrolling container to lazyload correctly.
      // With fullscreen parallax, the body overflow is hidden for parallax
      // container own scrollbar, hence we need to provide the option.
      $blazy = empty($attributes['data-blazy']) ? [] : Json::decode($attributes['data-blazy']);
      $blazy['container'] = '.is-b-scroll';
      $attributes['data-blazy'] = Json::encode($blazy);
    }
  }

  /**
   * Provides the .gridstack__inner container attributes.
   *
   * @todo this is not implemented, yet, only advanced usages with ungridding.
   * The idea is to destroy all grids, and still take advantage of GridStack
   * advanced and flexible stylings such as at Layout Builder pages. One sample
   * valid usage is a parallax page which doesn't need grids, but needs styling.
   * Since this introduces complications and requires custom theming, no UI is
   * provided for now. Anyone who knows theming should be easily adding _ungrid
   * via a hook_alter, and theme away as needed.
   */
  protected function contentAttributes(array &$attributes, array &$settings) {
    if (!empty($settings['_ungrid']) && $settings['_level'] == GridStackDefault::LEVEL_ROOT) {

      // @todo $attributes['class'] = $this->containerClasses();
      $attributes['class'][] = 'gridstack__inner';
    }
  }

  /**
   * Returns the .gridstack nested container attributes.
   */
  protected function nestedContainerAttributes(array &$settings) {
    $attributes['class'] = $this->nestedContainerClasses;
    $index = isset($settings['delta']) ? $settings['delta'] + 1 : 0;

    // Required by dynamic styling.
    $attributes['class'][] = 'gridstack--' . $index;

    // Optional, not really important.
    if (!$this
      ->getSetting('no_classes')) {
      $attributes['class'][] = 'gridstack--nested';
    }

    // Provides configurable attributes via Layout Builder.
    $this
      ->attributes($attributes, $settings);

    // This one is a container, not a registered region for layouts. Yet
    // provides the consistent selectors for editor live preview styling.
    if ($this
      ->getSetting('_ipe') && $settings['_level'] == GridStackDefault::LEVEL_NESTED) {
      $attributes['data-region'] = $settings['rid'];
    }
    return $attributes;
  }

  /**
   * Provides both CSS grid and js-driven attributes configurable via UI.
   */
  protected function attributes(array &$attributes, array $settings) {

    // Bail out if not configurable via Layout Builder alike.
    if (!empty($settings['_stylizer'])) {
      $this->stylizer
        ->attributes($attributes, $settings);

      // Provides styles per item to be aggregated at top level.
      if ($styles = $this->stylizer
        ->styles($attributes, $settings)) {
        $this
          ->setStyles($styles);
      }
    }
  }

  /**
   * Returns the module feature CSS classes, not available at CSS frameworks.
   */
  protected function getVersionClasses() {
    return $this->stylizer
      ->getInternalClasses();
  }

  /**
   * Returns options which make sense for preview at Layout Builder page.
   */
  public function previewOptions() {
    return [];
  }

}

Classes

Namesort descending Description
GridStackEnginePluginBase Provides base class for all gridstack layout engines.