Chris@18: layoutTempstoreRepository = $layout_tempstore_repository; Chris@18: $this->messenger = $messenger; Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { Chris@18: return new static( Chris@18: $configuration, Chris@18: $plugin_id, Chris@18: $plugin_definition, Chris@18: $container->get('layout_builder.tempstore_repository'), Chris@18: $container->get('messenger') Chris@18: ); Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function getInfo() { Chris@18: return [ Chris@18: '#section_storage' => NULL, Chris@18: '#pre_render' => [ Chris@18: [$this, 'preRender'], Chris@18: ], Chris@18: ]; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Pre-render callback: Renders the Layout Builder UI. Chris@18: */ Chris@18: public function preRender($element) { Chris@18: if ($element['#section_storage'] instanceof SectionStorageInterface) { Chris@18: $element['layout_builder'] = $this->layout($element['#section_storage']); Chris@18: } Chris@18: return $element; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Renders the Layout UI. Chris@18: * Chris@18: * @param \Drupal\layout_builder\SectionStorageInterface $section_storage Chris@18: * The section storage. Chris@18: * Chris@18: * @return array Chris@18: * A render array. Chris@18: */ Chris@18: protected function layout(SectionStorageInterface $section_storage) { Chris@18: $this->prepareLayout($section_storage); Chris@18: Chris@18: $output = []; Chris@18: if ($this->isAjax()) { Chris@18: $output['status_messages'] = [ Chris@18: '#type' => 'status_messages', Chris@18: ]; Chris@18: } Chris@18: $count = 0; Chris@18: for ($i = 0; $i < $section_storage->count(); $i++) { Chris@18: $output[] = $this->buildAddSectionLink($section_storage, $count); Chris@18: $output[] = $this->buildAdministrativeSection($section_storage, $count); Chris@18: $count++; Chris@18: } Chris@18: $output[] = $this->buildAddSectionLink($section_storage, $count); Chris@18: $output['#attached']['library'][] = 'layout_builder/drupal.layout_builder'; Chris@18: // As the Layout Builder UI is typically displayed using the frontend theme, Chris@18: // it is not marked as an administrative page at the route level even though Chris@18: // it performs an administrative task. Mark this as an administrative page Chris@18: // for JavaScript. Chris@18: $output['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE; Chris@18: $output['#type'] = 'container'; Chris@18: $output['#attributes']['id'] = 'layout-builder'; Chris@18: $output['#attributes']['class'][] = 'layout-builder'; Chris@18: // Mark this UI as uncacheable. Chris@18: $output['#cache']['max-age'] = 0; Chris@18: return $output; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Prepares a layout for use in the UI. Chris@18: * Chris@18: * @param \Drupal\layout_builder\SectionStorageInterface $section_storage Chris@18: * The section storage. Chris@18: */ Chris@18: protected function prepareLayout(SectionStorageInterface $section_storage) { Chris@18: // If the layout has pending changes, add a warning. Chris@18: if ($this->layoutTempstoreRepository->has($section_storage)) { Chris@18: $this->messenger->addWarning($this->t('You have unsaved changes.')); Chris@18: } Chris@18: // If the layout is an override that has not yet been overridden, copy the Chris@18: // sections from the corresponding default. Chris@18: elseif ($section_storage instanceof OverridesSectionStorageInterface && !$section_storage->isOverridden()) { Chris@18: $sections = $section_storage->getDefaultSectionStorage()->getSections(); Chris@18: foreach ($sections as $section) { Chris@18: $section_storage->appendSection($section); Chris@18: } Chris@18: $this->layoutTempstoreRepository->set($section_storage); Chris@18: } Chris@18: } Chris@18: Chris@18: /** Chris@18: * Builds a link to add a new section at a given delta. Chris@18: * Chris@18: * @param \Drupal\layout_builder\SectionStorageInterface $section_storage Chris@18: * The section storage. Chris@18: * @param int $delta Chris@18: * The delta of the section to splice. Chris@18: * Chris@18: * @return array Chris@18: * A render array for a link. Chris@18: */ Chris@18: protected function buildAddSectionLink(SectionStorageInterface $section_storage, $delta) { Chris@18: $storage_type = $section_storage->getStorageType(); Chris@18: $storage_id = $section_storage->getStorageId(); Chris@18: Chris@18: // If the delta and the count are the same, it is either the end of the Chris@18: // layout or an empty layout. Chris@18: if ($delta === count($section_storage)) { Chris@18: if ($delta === 0) { Chris@18: $title = $this->t('Add Section'); Chris@18: } Chris@18: else { Chris@18: $title = $this->t('Add Section at end of layout'); Chris@18: } Chris@18: } Chris@18: // If the delta and the count are different, it is either the beginning of Chris@18: // the layout or in between two sections. Chris@18: else { Chris@18: if ($delta === 0) { Chris@18: $title = $this->t('Add Section at start of layout'); Chris@18: } Chris@18: else { Chris@18: $title = $this->t('Add Section between @first and @second', ['@first' => $delta, '@second' => $delta + 1]); Chris@18: } Chris@18: } Chris@18: Chris@18: return [ Chris@18: 'link' => [ Chris@18: '#type' => 'link', Chris@18: '#title' => $title, Chris@18: '#url' => Url::fromRoute('layout_builder.choose_section', Chris@18: [ Chris@18: 'section_storage_type' => $storage_type, Chris@18: 'section_storage' => $storage_id, Chris@18: 'delta' => $delta, Chris@18: ], Chris@18: [ Chris@18: 'attributes' => [ Chris@18: 'class' => [ Chris@18: 'use-ajax', Chris@18: 'layout-builder__link', Chris@18: 'layout-builder__link--add', Chris@18: ], Chris@18: 'data-dialog-type' => 'dialog', Chris@18: 'data-dialog-renderer' => 'off_canvas', Chris@18: ], Chris@18: ] Chris@18: ), Chris@18: ], Chris@18: '#type' => 'container', Chris@18: '#attributes' => [ Chris@18: 'class' => ['layout-builder__add-section'], Chris@18: 'data-layout-builder-highlight-id' => $this->sectionAddHighlightId($delta), Chris@18: ], Chris@18: ]; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Builds the render array for the layout section while editing. Chris@18: * Chris@18: * @param \Drupal\layout_builder\SectionStorageInterface $section_storage Chris@18: * The section storage. Chris@18: * @param int $delta Chris@18: * The delta of the section. Chris@18: * Chris@18: * @return array Chris@18: * The render array for a given section. Chris@18: */ Chris@18: protected function buildAdministrativeSection(SectionStorageInterface $section_storage, $delta) { Chris@18: $storage_type = $section_storage->getStorageType(); Chris@18: $storage_id = $section_storage->getStorageId(); Chris@18: $section = $section_storage->getSection($delta); Chris@18: Chris@18: $layout = $section->getLayout(); Chris@18: $build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE); Chris@18: $layout_definition = $layout->getPluginDefinition(); Chris@18: Chris@18: $region_labels = $layout_definition->getRegionLabels(); Chris@18: foreach ($layout_definition->getRegions() as $region => $info) { Chris@18: if (!empty($build[$region])) { Chris@18: foreach (Element::children($build[$region]) as $uuid) { Chris@18: $build[$region][$uuid]['#attributes']['class'][] = 'js-layout-builder-block'; Chris@18: $build[$region][$uuid]['#attributes']['class'][] = 'layout-builder-block'; Chris@18: $build[$region][$uuid]['#attributes']['data-layout-block-uuid'] = $uuid; Chris@18: $build[$region][$uuid]['#attributes']['data-layout-builder-highlight-id'] = $this->blockUpdateHighlightId($uuid); Chris@18: $build[$region][$uuid]['#contextual_links'] = [ Chris@18: 'layout_builder_block' => [ Chris@18: 'route_parameters' => [ Chris@18: 'section_storage_type' => $storage_type, Chris@18: 'section_storage' => $storage_id, Chris@18: 'delta' => $delta, Chris@18: 'region' => $region, Chris@18: 'uuid' => $uuid, Chris@18: ], Chris@18: // Add metadata about the current operations available in Chris@18: // contextual links. This will invalidate the client-side cache of Chris@18: // links that were cached before the 'move' link was added. Chris@18: // @see layout_builder.links.contextual.yml Chris@18: 'metadata' => [ Chris@18: 'operations' => 'move:update:remove', Chris@18: ], Chris@18: ], Chris@18: ]; Chris@18: } Chris@18: } Chris@18: Chris@18: $build[$region]['layout_builder_add_block']['link'] = [ Chris@18: '#type' => 'link', Chris@18: // Add one to the current delta since it is zero-indexed. Chris@18: '#title' => $this->t('Add Block in section @section, @region region', ['@section' => $delta + 1, '@region' => $region_labels[$region]]), Chris@18: '#url' => Url::fromRoute('layout_builder.choose_block', Chris@18: [ Chris@18: 'section_storage_type' => $storage_type, Chris@18: 'section_storage' => $storage_id, Chris@18: 'delta' => $delta, Chris@18: 'region' => $region, Chris@18: ], Chris@18: [ Chris@18: 'attributes' => [ Chris@18: 'class' => [ Chris@18: 'use-ajax', Chris@18: 'layout-builder__link', Chris@18: 'layout-builder__link--add', Chris@18: ], Chris@18: 'data-dialog-type' => 'dialog', Chris@18: 'data-dialog-renderer' => 'off_canvas', Chris@18: ], Chris@18: ] Chris@18: ), Chris@18: ]; Chris@18: $build[$region]['layout_builder_add_block']['#type'] = 'container'; Chris@18: $build[$region]['layout_builder_add_block']['#attributes'] = [ Chris@18: 'class' => ['layout-builder__add-block'], Chris@18: 'data-layout-builder-highlight-id' => $this->blockAddHighlightId($delta, $region), Chris@18: ]; Chris@18: $build[$region]['layout_builder_add_block']['#weight'] = 1000; Chris@18: $build[$region]['#attributes']['data-region'] = $region; Chris@18: $build[$region]['#attributes']['class'][] = 'layout-builder__region'; Chris@18: $build[$region]['#attributes']['class'][] = 'js-layout-builder-region'; Chris@18: $build[$region]['#attributes']['role'] = 'group'; Chris@18: $build[$region]['#attributes']['aria-label'] = $this->t('@region region in section @section', [ Chris@18: '@region' => $info['label'], Chris@18: '@section' => $delta + 1, Chris@18: ]); Chris@18: Chris@18: // Get weights of all children for use by the region label. Chris@18: $weights = array_map(function ($a) { Chris@18: return isset($a['#weight']) ? $a['#weight'] : 0; Chris@18: }, $build[$region]); Chris@18: Chris@18: // The region label is made visible when the move block dialog is open. Chris@18: $build[$region]['region_label'] = [ Chris@18: '#type' => 'container', Chris@18: '#attributes' => [ Chris@18: 'class' => ['layout__region-info', 'layout-builder__region-label'], Chris@18: // A more detailed version of this information is already read by Chris@18: // screen readers, so this label can be hidden from them. Chris@18: 'aria-hidden' => TRUE, Chris@18: ], Chris@18: '#markup' => $this->t('Region: @region', ['@region' => $info['label']]), Chris@18: // Ensures the region label is displayed first. Chris@18: '#weight' => min($weights) - 1, Chris@18: ]; Chris@18: } Chris@18: Chris@18: $build['#attributes']['data-layout-update-url'] = Url::fromRoute('layout_builder.move_block', [ Chris@18: 'section_storage_type' => $storage_type, Chris@18: 'section_storage' => $storage_id, Chris@18: ])->toString(); Chris@18: Chris@18: $build['#attributes']['data-layout-delta'] = $delta; Chris@18: $build['#attributes']['class'][] = 'layout-builder__layout'; Chris@18: $build['#attributes']['data-layout-builder-highlight-id'] = $this->sectionUpdateHighlightId($delta); Chris@18: Chris@18: return [ Chris@18: '#type' => 'container', Chris@18: '#attributes' => [ Chris@18: 'class' => ['layout-builder__section'], Chris@18: 'role' => 'group', Chris@18: 'aria-label' => $this->t('Section @section', ['@section' => $delta + 1]), Chris@18: ], Chris@18: 'remove' => [ Chris@18: '#type' => 'link', Chris@18: '#title' => $this->t('Remove section @section', ['@section' => $delta + 1]), Chris@18: '#url' => Url::fromRoute('layout_builder.remove_section', [ Chris@18: 'section_storage_type' => $storage_type, Chris@18: 'section_storage' => $storage_id, Chris@18: 'delta' => $delta, Chris@18: ]), Chris@18: '#attributes' => [ Chris@18: 'class' => [ Chris@18: 'use-ajax', Chris@18: 'layout-builder__link', Chris@18: 'layout-builder__link--remove', Chris@18: ], Chris@18: 'data-dialog-type' => 'dialog', Chris@18: 'data-dialog-renderer' => 'off_canvas', Chris@18: ], Chris@18: ], Chris@18: // The section label is added to sections without a "Configure Section" Chris@18: // link, and is only visible when the move block dialog is open. Chris@18: 'section_label' => [ Chris@18: '#markup' => $this->t('', ['@section' => $delta + 1]), Chris@18: '#access' => !$layout instanceof PluginFormInterface, Chris@18: ], Chris@18: 'configure' => [ Chris@18: '#type' => 'link', Chris@18: // There are two instances of @section, the one wrapped in Chris@18: // .visually-hidden is for screen readers. The one wrapped in Chris@18: // .layout-builder__section-label is only visible when the Chris@18: // move block dialog is open and it is not seen by screen readers. Chris@18: '#title' => $this->t('Configure section @section', ['@section' => $delta + 1]), Chris@18: '#access' => $layout instanceof PluginFormInterface, Chris@18: '#url' => Url::fromRoute('layout_builder.configure_section', [ Chris@18: 'section_storage_type' => $storage_type, Chris@18: 'section_storage' => $storage_id, Chris@18: 'delta' => $delta, Chris@18: ]), Chris@18: '#attributes' => [ Chris@18: 'class' => [ Chris@18: 'use-ajax', Chris@18: 'layout-builder__link', Chris@18: 'layout-builder__link--configure', Chris@18: ], Chris@18: 'data-dialog-type' => 'dialog', Chris@18: 'data-dialog-renderer' => 'off_canvas', Chris@18: ], Chris@18: ], Chris@18: 'layout-builder__section' => $build, Chris@18: ]; Chris@18: } Chris@18: Chris@18: }