Skip to content
Snippets Groups Projects
Commit ff23c67b authored by Florent Torregrosa's avatar Florent Torregrosa
Browse files

Issue #3490687 by grimreaper: Layout Builder: stylize main form/page

- add margin and "buttonify" links in actions everywhere
- add support for section_library
parent 64410067
No related branches found
No related tags found
1 merge request!240Issue #3490687 by grimreaper: WIP: Layout Builder: stylize main form/page
Pipeline #374128 passed
/**
* @file
* Layout Builder UI styling.
*/
.layout-builder__add-section {
outline-width: 2px;
outline-style: dashed;
outline-color: var(--bs-secondary-border-subtle);
}
.layout-builder__section {
margin-bottom: 1.5em;
}
.layout-builder__section .ui-sortable-helper {
outline-width: 2px;
outline-style: solid;
outline-color: var(--bs-light-border-subtle);
background-color: #fff;
}
.layout-builder__section .ui-state-drop {
margin: 1.25rem;
padding: 1.875rem;
outline-width: 2px;
outline-style: dashed;
outline-color: var(--bs-warning);
background-color: var(--bs-warning-bg-subtle);
}
.layout-builder__region {
outline-width: 2px;
outline-style: dashed;
outline-color: var(--bs-primary);
}
.layout-builder-block {
padding: 1.5em;
cursor: move;
background-color: #fff;
}
.layout-builder-block [tabindex="-1"] {
pointer-events: none;
}
.layout-builder--content-preview-disabled .layout-builder-block {
margin: 0;
border-bottom: 2px dashed var(--bs-secondary-border-subtle);
}
/* Label when "content preview" is disabled. */
.layout-builder-block__content-preview-placeholder-label {
margin: 0;
text-align: center;
font-size: 1.429em;
line-height: 1.4;
}
.layout-builder__add-section.is-layout-builder-highlighted {
margin-bottom: calc(1.5em - 0.5rem);
outline: none;
}
.layout-builder__layout.is-layout-builder-highlighted,
.layout-builder-block.is-layout-builder-highlighted,
.layout-builder__add-block.is-layout-builder-highlighted {
position: relative;
z-index: 1;
margin: -0.25rem -2px;
}
.layout-builder__add-block.is-layout-builder-highlighted,
.layout-builder__add-section.is-layout-builder-highlighted,
.layout-builder__layout.is-layout-builder-highlighted::before,
.layout-builder__layout.is-layout-builder-highlighted,
.layout-builder-block.is-layout-builder-highlighted {
border: 4px solid #000;
}
.layout-builder__section-label {
color: var(--bs-primary);
}
.layout-builder__region-label {
display: none;
}
.layout-builder--move-blocks-active .layout-builder__region-label {
display: block;
}
.layout-builder--move-blocks-active .layout-builder__section-label {
display: inline;
}
.layout__region-info {
padding: 0.5em;
text-align: center;
border-bottom: 2px dashed var(--bs-secondary-border-subtle);
}
/**
* Remove "You have unsaved changes" warning because Layout Builder always has
* unsaved changes until "Save layout" is submitted.
* @todo create issue for todo.
*/
.layout-builder-components-table .tabledrag-changed-warning {
display: none !important;
}
<?php
declare(strict_types=1);
namespace Drupal\ui_suite_bootstrap\Element;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Url;
use Drupal\ui_suite_bootstrap\Utility\Bootstrap;
use Drupal\ui_suite_bootstrap\Utility\Element;
/**
* Element Prerender methods for layout builder.
*/
class ElementPreRenderLayoutBuilder implements TrustedCallbackInterface {
/**
* Weight to ensure the section label is placed first.
*/
public const SECTION_LABEL_WEIGHT = -1;
/**
* Handle styling for layout builder element.
*/
public static function preRenderLayoutBuilder(array $element): array {
$element_object = Element::create($element);
if (!isset($element_object->layout_builder)) {
return $element;
}
// Main wrapper.
$element_object->layout_builder->addClass([
'border',
'border-3',
'border-primary',
'bg-white',
'p-4',
'pb-2',
]);
/** @var \Drupal\ui_suite_bootstrap\Utility\Element $layoutBuilderArea */
foreach ($element_object->layout_builder->children() as $layoutBuilderArea) {
// Add section.
if (isset($layoutBuilderArea->link) && $layoutBuilderArea->link->isType('link')) {
$url = $layoutBuilderArea->link->getProperty('url');
if ($url instanceof Url && $url->getRouteName() == 'layout_builder.choose_section') {
// Wrapper.
$layoutBuilderArea->addClass([
'mb-3',
'py-4',
'text-center',
'bg-light',
]);
// Link.
$layoutBuilderArea->link->addClass([
'btn',
'btn-secondary',
]);
$layoutBuilderArea->link->setIcon(Bootstrap::icon('plus-lg'));
}
}
// Section label.
// Display section label first. So we can display action links with icon
// only.
if (isset($layoutBuilderArea->section_label)) {
$layoutBuilderArea->section_label->setProperty('access', TRUE);
$layoutBuilderArea->section_label->setProperty('weight', static::SECTION_LABEL_WEIGHT);
}
// Remove link.
if (isset($layoutBuilderArea->remove) && $layoutBuilderArea->remove->isType('link')) {
$url = $layoutBuilderArea->remove->getProperty('url');
if ($url instanceof Url && $url->getRouteName() == 'layout_builder.remove_section') {
$layoutBuilderArea->remove->addClass('mx-1');
$layoutBuilderArea->remove->setIcon(Bootstrap::icon('trash'));
$layoutBuilderArea->remove->setProperty('icon_position', 'icon_only');
}
}
// Configure link.
if (isset($layoutBuilderArea->configure) && $layoutBuilderArea->configure->isType('link')) {
$url = $layoutBuilderArea->configure->getProperty('url');
if ($url instanceof Url && $url->getRouteName() == 'layout_builder.configure_section') {
$layoutBuilderArea->configure->addClass('mx-1');
$layoutBuilderArea->configure->setIcon(Bootstrap::icon('pencil-fill'));
$layoutBuilderArea->configure->setProperty('icon_position', 'icon_only');
}
}
// Section.
if (isset($layoutBuilderArea->{'layout-builder__section'})) {
$section = $layoutBuilderArea->{'layout-builder__section'};
foreach ($section->children() as $region) {
// Add block.
if (isset($region->layout_builder_add_block)) {
/** @var \Drupal\ui_suite_bootstrap\Utility\Element $addBlockArea */
$addBlockArea = $region->layout_builder_add_block;
if (isset($addBlockArea->link) && $addBlockArea->link->isType('link')) {
$url = $addBlockArea->link->getProperty('url');
if ($url instanceof Url && $url->getRouteName() == 'layout_builder.choose_block') {
// Wrapper.
$addBlockArea->addClass([
'py-4',
'text-center',
'bg-primary-subtle',
]);
// Link.
$addBlockArea->link->addClass([
'btn',
'btn-primary',
]);
$addBlockArea->link->setIcon(Bootstrap::icon('plus-lg'));
}
}
}
}
}
// Section Library: Add this template to library.
if (isset($layoutBuilderArea->add_template_to_library) && $layoutBuilderArea->add_template_to_library->isType('link')) {
$url = $layoutBuilderArea->add_template_to_library->getProperty('url');
if ($url instanceof Url && $url->getRouteName() == 'section_library.add_template_to_library') {
$layoutBuilderArea->add_template_to_library->addClass([
'btn',
'btn-outline-secondary',
]);
$layoutBuilderArea->add_template_to_library->setIcon(Bootstrap::icon('folder-plus'));
$layoutBuilderArea->add_template_to_library->appendProperty('theme_wrappers', [
'container' => [
'#attributes' => [
'class' => [
'mb-3',
'text-center',
],
],
],
]);
}
}
// Section Library: Import from library.
if (isset($layoutBuilderArea->choose_template_from_library) && $layoutBuilderArea->choose_template_from_library->isType('link')) {
$url = $layoutBuilderArea->choose_template_from_library->getProperty('url');
if ($url instanceof Url && $url->getRouteName() == 'section_library.choose_template_from_library') {
$layoutBuilderArea->choose_template_from_library->addClass([
'btn',
'btn-outline-secondary',
'ms-3',
]);
$layoutBuilderArea->choose_template_from_library->setIcon(Bootstrap::icon('download'));
}
}
// Section Library: Add to library.
if (isset($layoutBuilderArea->add_to_library) && $layoutBuilderArea->add_to_library->isType('link')) {
$url = $layoutBuilderArea->add_to_library->getProperty('url');
if ($url instanceof Url && $url->getRouteName() == 'section_library.add_section_to_library') {
$layoutBuilderArea->add_to_library->addClass('mx-1');
$layoutBuilderArea->add_to_library->setIcon(Bootstrap::icon('folder-plus'));
$layoutBuilderArea->add_to_library->setProperty('icon_position', 'icon_only');
}
}
}
return $element;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks(): array {
return ['preRenderLayoutBuilder'];
}
}
......@@ -17,10 +17,6 @@ class ElementProcessActions {
*/
public static function processActions(array &$element, FormStateInterface $form_state, array &$complete_form): array {
$element_object = Element::create($element);
if (!$element_object->getProperty('isLayoutBuilder')) {
return $element;
}
$element_object->addClass('mt-3');
// Change links into buttons.
......
......@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\ui_suite_bootstrap\HookHandler;
use Drupal\ui_suite_bootstrap\Element\ElementPreRenderDropbutton;
use Drupal\ui_suite_bootstrap\Element\ElementPreRenderLayoutBuilder;
use Drupal\ui_suite_bootstrap\Element\ElementPreRenderLink;
use Drupal\ui_suite_bootstrap\Element\ElementProcessActions;
use Drupal\ui_suite_bootstrap\Element\ElementProcessAjax;
......@@ -216,6 +217,14 @@ class ElementInfoAlter {
}
}
// Layout Builder.
if (isset($info['layout_builder'])) {
$info['layout_builder']['#pre_render'][] = [
ElementPreRenderLayoutBuilder::class,
'preRenderLayoutBuilder',
];
}
// Link.
if (isset($info['link'])) {
if (!isset($info['link']['#pre_render'])) {
......
......@@ -31,10 +31,10 @@ class FormAlter {
];
}
if ($this->isLayoutBuilderForm($form, $form_id)) {
if ($this->isOffcanvasForm($form, $form_id)) {
// Even with the after build, some elements like actions is not marked,
// so marked it directly.
static::markLayoutBuilder($form);
static::markOffcanvas($form);
// Use #after_build otherwise we do not have access to all subform
// elements.
$form['#after_build'][] = [
......@@ -43,25 +43,17 @@ class FormAlter {
];
}
// Default styling for Layout Builder "main" form.
// There is no specific form ID to target as the base form ID is dynamic in
// LayoutBuilderEntityFormTrait.
if (\str_ends_with($form_id, '_layout_builder_form')) {
$this->alterLayoutBuilderForm($form, $formState);
}
// Default styling for views bulk actions forms. There is no specific form
// ID to target.
if (!\str_starts_with($form['#id'], 'views-form')) {
return;
}
if (isset($form['header'])) {
$headerElements = Element::children($form['header']);
foreach ($headerElements as $headerElement) {
if (!\str_ends_with($headerElement, '_bulk_form')) {
continue;
}
$form['header'][$headerElement]['#attributes']['class'][] = 'row';
$form['header'][$headerElement]['#attributes']['class'][] = 'row-cols-auto';
$form['header'][$headerElement]['#attributes']['class'][] = 'align-items-end';
if (isset($form['header'][$headerElement]['actions'])) {
$form['header'][$headerElement]['actions']['#attributes']['class'][] = 'mb-3';
}
}
if (\str_starts_with($form['#id'], 'views-form')) {
$this->alterViewsBulkActionForm($form);
}
}
......@@ -77,7 +69,7 @@ class FormAlter {
* Form element #after_build callback.
*/
public static function afterBuildMarkLayoutBuilder(array $element, FormStateInterface $form_state): array {
static::markLayoutBuilder($element);
static::markOffcanvas($element);
return $element;
}
......@@ -99,7 +91,7 @@ class FormAlter {
}
/**
* Detect if Layout Builder form.
* Detect if Layout Builder offcanvas form.
*
* @param array $form
* The form structure.
......@@ -107,63 +99,91 @@ class FormAlter {
* The form ID.
*
* @return bool
* True for gin form.
* True for offcanvas form.
*
* @see gin_lb_is_layout_builder_form_id()
*/
protected function isLayoutBuilderForm(array &$form, string $form_id): bool {
protected function isOffcanvasForm(array &$form, string $form_id): bool {
$form_ids = [
'editor_image_dialog',
'form-autocomplete',
'layout_builder_add_block',
'layout_builder_block_move',
'layout_builder_configure_section',
'layout_builder_remove_block',
'layout_builder_remove_section',
'layout_builder_update_block',
'media_image_edit_form',
'media_library_add_form_oembed',
'media_library_add_form_upload',
'section_library_add_section_to_library',
'section_library_add_template_to_library',
];
if (\in_array($form_id, $form_ids, TRUE)) {
return TRUE;
}
$form_id_contains = [
'layout_builder_translate_form',
'views_form_media_library_widget_',
];
foreach ($form_id_contains as $form_id_contain) {
if (\strpos($form_id, $form_id_contain) !== FALSE) {
if (\str_contains($form_id, $form_id_contain)) {
return TRUE;
}
}
if (\in_array($form_id, $form_ids, TRUE)) {
return TRUE;
}
if ($form_id === 'views_exposed_form' && isset($form['#id']) && $form['#id'] === 'views-exposed-form-media-library-widget') {
return TRUE;
}
if (\strpos($form_id, 'layout_builder_form') !== FALSE) {
return TRUE;
}
return FALSE;
}
/**
* Set isLayoutBuilder to all form elements.
* Set isOffcanvas to all form elements.
*
* @param array $form
* The form or form element which children should have form id attached.
*/
protected static function markLayoutBuilder(array &$form): void {
protected static function markOffcanvas(array &$form): void {
foreach (Element::children($form) as $child) {
if (!isset($form[$child]['#isLayoutBuilder'])) {
$form[$child]['#isLayoutBuilder'] = TRUE;
if (!isset($form[$child]['#isOffcanvas'])) {
$form[$child]['#isOffcanvas'] = TRUE;
}
static::markOffcanvas($form[$child]);
}
}
/**
* Default styling for Layout Builder "main" form.
*
* @param array $form
* The form structure.
* @param \Drupal\Core\Form\FormStateInterface $formState
* The form state.
*/
protected function alterLayoutBuilderForm(array &$form, FormStateInterface $formState): void {
$form['actions']['#attributes']['class'][] = 'mb-3';
// Move preview checkbox out of actions so buttons are aligned.
$form['preview_toggle'] = $form['actions']['preview_toggle'];
unset($form['actions']['preview_toggle']);
}
/**
* Default styling for views bulk actions forms.
*
* @param array $form
* The form structure.
*/
protected function alterViewsBulkActionForm(array &$form): void {
if (!isset($form['header'])) {
return;
}
$headerElements = Element::children($form['header']);
foreach ($headerElements as $headerElement) {
if (!\str_ends_with($headerElement, '_bulk_form')) {
continue;
}
$form['header'][$headerElement]['#attributes']['class'][] = 'row';
$form['header'][$headerElement]['#attributes']['class'][] = 'row-cols-auto';
$form['header'][$headerElement]['#attributes']['class'][] = 'align-items-end';
if (isset($form['header'][$headerElement]['actions'])) {
$form['header'][$headerElement]['actions']['#attributes']['class'][] = 'mb-3';
}
static::markLayoutBuilder($form[$child]);
}
}
......
......@@ -32,7 +32,7 @@ class PreprocessDetailsAccordion extends PreprocessFormElement {
/** @var array $accordion_attributes */
$accordion_attributes = $this->element->getProperty('accordion_attributes', []);
$accordion_attributes = new Attribute($accordion_attributes);
if ($this->element->getProperty('isLayoutBuilder')) {
if ($this->element->getProperty('isOffcanvas')) {
$accordion_attributes->addClass('accordion-flush');
$style = $accordion_attributes->offsetGet('style') ?? '';
$accordion_attributes->setAttribute('style', $style . '--bs-accordion-body-padding-x: 0');
......
......@@ -50,9 +50,9 @@ class PreprocessFieldset extends PreprocessFormElement {
]);
$label_attributes->addClass('col-form-label');
}
// In Layout Builder, ensure the fieldset legend has normal size for
// checkboxes and radios.
elseif ($this->element->isType(['checkboxes', 'radios']) && $this->element->getProperty('isLayoutBuilder')) {
// In Layout Builder/Offcanvas, ensure the fieldset legend has normal size
// for checkboxes and radios.
elseif ($this->element->isType(['checkboxes', 'radios']) && $this->element->getProperty('isOffcanvas')) {
$label_attributes->addClass('fs-6');
}
// Display fieldset as card by default.
......
......@@ -7,7 +7,6 @@ namespace Drupal\ui_suite_bootstrap\HookHandler;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Routing\CurrentRouteMatch;
use Drupal\ui_suite_bootstrap\Utility\Variables;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
......@@ -108,56 +107,4 @@ class ThemeSuggestionsAlter implements ContainerInjectionInterface {
}
}
/**
* Detect Layout Builder route.
*
* @param array $variables
* The theme variables.
*
* @return bool
* True if Layout Builder route. False otherwise.
*
* @see gin_lb_theme_suggestions_alter()
*/
protected function isLayoutBuilderRoute(array $variables): bool {
$route_name = $this->currentRouteMatch->getRouteName();
if (\in_array($route_name, [
'editor.image_dialog',
'editor.link_dialog',
'editor.media_dialog',
'layout_builder.add_block',
// @see https://www.drupal.org/project/drupal/issues/3134371
'layout_builder.change_section_layout',
'layout_builder.choose_block',
'layout_builder.choose_inline_block',
'layout_builder.choose_section',
// @see https://www.drupal.org/project/drupal/issues/3134371
'layout_builder.configure_changed_section_layout',
'layout_builder.remove_block',
'layout_builder.remove_section',
'media_library.ui',
'section_library.add_section_to_library',
'section_library.add_template_to_library',
'view.media_library.widget',
'view.media_library.widget_table',
], TRUE)) {
return TRUE;
}
// For ajax the route is views.ajax
// So a look to the suggestions help.
if ($route_name === 'views.ajax') {
$request = $this->requestStack->getCurrentRequest();
if ($request && $request->query->get('media_library_opener_id')) {
return TRUE;
}
$view = isset($variables['view']) && $variables['view'] instanceof ViewExecutable;
if ($view && $variables['view']->id() === 'media_library') {
return TRUE;
}
}
return FALSE;
}
}
......@@ -66,6 +66,7 @@ class Bootstrap {
// Outline danger class.
\t('Discard changes')->render() => 'outline-danger',
\t('Revert to defaults')->render() => 'outline-danger',
],
// Text containing these words anywhere in the string are checked last.
......@@ -126,7 +127,7 @@ class Bootstrap {
break;
case 'contains':
if (\strpos(\mb_strtolower($string), \mb_strtolower($text)) !== FALSE) {
if (\str_contains(\mb_strtolower($string), \mb_strtolower($text))) {
return $class;
}
break;
......@@ -200,6 +201,7 @@ class Bootstrap {
\t('Discard')->render() => $iconInfos + ['iconId' => 'trash'],
\t('Remove')->render() => $iconInfos + ['iconId' => 'trash'],
\t('Reset')->render() => $iconInfos + ['iconId' => 'trash'],
\t('Revert')->render() => $iconInfos + ['iconId' => 'arrow-counterclockwise'],
\t('Search')->render() => $iconInfos + ['iconId' => 'search'],
\t('Upload')->render() => $iconInfos + ['iconId' => 'upload'],
\t('Preview')->render() => $iconInfos + ['iconId' => 'eye-fill'],
......@@ -231,7 +233,7 @@ class Bootstrap {
break;
case 'contains':
if (\strpos(\mb_strtolower($string), \mb_strtolower($text)) !== FALSE) {
if (\str_contains(\mb_strtolower($string), \mb_strtolower($text))) {
return static::icon($icon['iconId'], $icon['packId'], $icon['settings']);
}
break;
......
......@@ -101,7 +101,7 @@ class Variables extends DrupalAttributes {
}
// Merge attributes from the element.
if (\strpos($property, 'attributes') !== FALSE) {
if (\str_contains($property, 'attributes')) {
// @phpstan-ignore-next-line
$this->setAttributes($this->element->getAttributes($property)->getArrayCopy(), $variable);
}
......
......@@ -59,6 +59,7 @@ libraries-override:
layout_builder/drupal.layout_builder:
css:
theme:
css/layout-builder.css: false
css/off-canvas.css: false
node/drupal.node.preview:
css:
......@@ -75,6 +76,10 @@ libraries-override:
commerce_checkout/form: false
commerce_checkout/login_pane: false
paragraphs/drupal.paragraphs.unpublished: false
section_library/section_library:
css:
theme:
css/section-library.css: false
ui_icons/ui_icons.autocomplete:
css:
component:
......@@ -99,6 +104,8 @@ libraries-extend:
- ui_suite_bootstrap/drupal.progress
core/drupal.tabledrag:
- ui_suite_bootstrap/drupal.tabledrag
layout_builder/drupal.layout_builder:
- ui_suite_bootstrap/drupal.layout_builder
media_library/view:
- ui_suite_bootstrap/media_library.theme
media_library/widget:
......
......@@ -73,6 +73,11 @@ drupal.dialog.off_canvas:
component:
assets/css/component/off-canvas.css: {}
drupal.layout_builder:
css:
theme:
assets/css/layout-builder/layout-builder.css: {}
drupal.message:
js:
assets/js/misc/message.js: {}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment