diff --git a/core/composer.json b/core/composer.json index 503704c..45d2c80 100644 --- a/core/composer.json +++ b/core/composer.json @@ -138,6 +138,7 @@ "drupal/taxonomy": "self.version", "drupal/telephone": "self.version", "drupal/text": "self.version", + "drupal/theme_layout": "self.version", "drupal/toolbar": "self.version", "drupal/tour": "self.version", "drupal/tracker": "self.version", diff --git a/core/modules/theme_layout/config/schema/theme_layout.schema.yml b/core/modules/theme_layout/config/schema/theme_layout.schema.yml new file mode 100644 index 0000000..388ba5d --- /dev/null +++ b/core/modules/theme_layout/config/schema/theme_layout.schema.yml @@ -0,0 +1,10 @@ +theme_settings.third_party.theme_layout: + type: mapping + label: 'Per-theme layout settings' + mapping: + layout: + type: string + label: 'Layout ID' + settings: + type: layout_plugin.settings.[%parent.id] + label: 'Layout settings' diff --git a/core/modules/theme_layout/tests/src/Functional/ThemeLayoutTest.php b/core/modules/theme_layout/tests/src/Functional/ThemeLayoutTest.php new file mode 100644 index 0000000..29029f6 --- /dev/null +++ b/core/modules/theme_layout/tests/src/Functional/ThemeLayoutTest.php @@ -0,0 +1,82 @@ +container->get('theme_installer')->install(['seven']); + $this->container->get('theme_handler')->setDefault('seven'); + + $this->drupalLogin($this->drupalCreateUser([ + 'access administration pages', + 'administer blocks', + 'administer themes', + 'view the administration theme', + ])); + $this->placeBlock('system_branding_block', ['region' => 'header', 'id' => 'branding']); + $this->placeBlock('system_messages_block', ['region' => 'help', 'id' => 'messages']); + $this->placeBlock('system_main_block', ['region' => 'content', 'id' => 'main']); + $this->placeBlock('system_powered_by_block', ['region' => 'content', 'id' => 'powered_by']); + } + + /** + */ + public function test() { + $this->drupalGet(Url::fromRoute('block.admin_display_theme', ['theme' => 'seven'])); + $this->assertRegionNames([ + 'branding' => 'header', + 'messages' => 'help', + 'main' => 'content', + 'powered_by' => 'content', + ]); + + $this->container->get('module_installer')->install(['theme_layout']); + + $this->drupalGet(Url::fromRoute('system.theme_settings_theme', ['theme' => 'seven'])); + $select = $this->assertSession()->selectExists('theme_layout'); + $this->assertSame('seven', $select->getValue()); + + $this->drupalGet(Url::fromRoute('block.admin_display_theme', ['theme' => 'seven'])); + $this->assertRegionNames([ + 'branding' => 'header', + 'messages' => 'help', + 'main' => 'content', + 'powered_by' => 'content', + ]); + } + + /** + * @todo. + */ + protected function assertRegionNames(array $mappings) { + $this->assertSession()->pageTextContains('Header'); + $this->assertSession()->pageTextContains('Pre-content'); + $this->assertSession()->pageTextContains('Breadcrumb'); + $this->assertSession()->pageTextContains('Highlighted'); + $this->assertSession()->pageTextContains('Help'); + $this->assertSession()->pageTextContains('Content'); + foreach ($mappings as $block_id => $region) { + $this->assertSame($region, $this->assertSession()->selectExists('blocks[' . $block_id . '][region]')->getValue()); + } + } + +} diff --git a/core/modules/theme_layout/theme_layout.info.yml b/core/modules/theme_layout/theme_layout.info.yml new file mode 100644 index 0000000..6151e9a --- /dev/null +++ b/core/modules/theme_layout/theme_layout.info.yml @@ -0,0 +1,8 @@ +name: 'Theme Layout' +type: module +description: 'Adds layout capabilities to themes.' +package: Core (Experimental) +version: VERSION +core: 8.x +dependencies: + - layout_discovery diff --git a/core/modules/theme_layout/theme_layout.install b/core/modules/theme_layout/theme_layout.install new file mode 100644 index 0000000..9a81b2f --- /dev/null +++ b/core/modules/theme_layout/theme_layout.install @@ -0,0 +1,40 @@ +listInfo(); + _theme_layout_install_theme_settings(array_keys($themes)); + + // Refresh the theme info now that the layout has been changed. + \Drupal::service('theme_handler')->refreshInfo(); +} + +/** + * Implements hook_themes_installed(). + */ +function theme_layout_themes_installed($theme_list) { + _theme_layout_install_theme_settings($theme_list); + + // Refresh the theme info now that the layout has been changed. + \Drupal::service('theme_handler')->refreshInfo(); +} + +/** + * @todo. + */ +function _theme_layout_install_theme_settings($themes) { + $layout_definitions = \Drupal::service('plugin.manager.core.layout')->getDefinitions(); + foreach (array_intersect($themes, array_keys($layout_definitions)) as $theme) { + \Drupal::configFactory()->getEditable($theme . '.settings') + ->set('third_party_settings.theme_layout.layout', $theme) + ->save(TRUE); + } +} diff --git a/core/modules/theme_layout/theme_layout.module b/core/modules/theme_layout/theme_layout.module new file mode 100644 index 0000000..80231a5 --- /dev/null +++ b/core/modules/theme_layout/theme_layout.module @@ -0,0 +1,139 @@ +' . t('About') . ''; + $output .= '

' . t('The Theme Layout module allows you to assign regions for your theme layout.') . '

'; + $output .= '

' . t('For more information, see the online documentation for the Theme Layout module.', [':theme-layout-documentation' => 'https://www.drupal.org/documentation/modules/@todo_once_module_name_is_decided_upon']) . '

'; + return $output; + } +} + +/** + * Implements hook_form_system_theme_settings_alter(). + */ +function theme_layout_form_system_theme_settings_alter(&$form, FormStateInterface $form_state) { + $build_info = $form_state->getBuildInfo(); + if (!isset($build_info['args'][0]) || !($theme = $build_info['args'][0])) { + return; + } + + $form['theme_layouts'] = [ + '#type' => 'details', + '#title' => t('Layout'), + '#open' => TRUE, + ]; + $layout_options = \Drupal::service('plugin.manager.core.layout')->getLayoutOptions(); + $form['theme_layouts']['theme_layout'] = [ + '#type' => 'select', + '#title' => t('Select a layout'), + '#options' => $layout_options, + '#default_value' => \Drupal::config($theme . '.settings')->get('third_party_settings.theme_layout.layout') ?: 'default', + ]; + // Color module removes values from $form_state, so run first. + array_unshift($form['#submit'], 'theme_layout_system_theme_settings_submit'); +} + +/** + * Form submission handler for the system theme settings form. + */ +function theme_layout_system_theme_settings_submit(array &$form, FormStateInterface $form_state) { + $theme = $form_state->getValue('theme'); + $config = \Drupal::configFactory()->getEditable($theme . '.settings'); + $old_layout = $config->get('third_party_settings.theme_layout.layout'); + $new_layout = $form_state->getValue('theme_layout'); + $config + ->set('third_party_settings.theme_layout.layout', $new_layout) + ->save(); + + // Refresh the theme info now that the layout has been changed. + \Drupal::service('theme_handler')->refreshInfo(); + + if ($new_layout !== $old_layout) { + // @todo Devise a mechanism for mapping old regions to new ones in + // https://www.drupal.org/node/2796877. + $layout_definition = \Drupal::service('plugin.manager.core.layout')->getLayout($new_layout); + $new_region = isset($layout_definition['default_region']) ? $layout_definition['default_region'] : key($layout_definition['regions']); + + /** @var \Drupal\block\BlockInterface[] $blocks */ + $blocks = \Drupal::entityTypeManager()->getStorage('block')->loadByProperties(['theme' => $theme]); + foreach ($blocks as $block) { + $block->setRegion($new_region)->save(); + } + } + + // Remove the value to prevent theme_settings_convert_to_config() from + // directly saving it. + $form_state->unsetValue('theme_layout'); +} + +/** + * Implements hook_system_info_alter(). + */ +function theme_layout_system_info_alter(array &$info, Extension $file, $type) { + if ($file->getType() === 'theme' && $layout_definition = \Drupal::service('plugin.manager.core.layout')->getDefinition(_theme_layout_get_layout_id($file->getName()), FALSE)) { + unset($info['regions'], $info['regions_hidden']); + foreach ($layout_definition->getRegions() as $region => $region_info) { + $info['regions'][$region] = (string) $region_info['label']; + if (isset($region_info['hidden'])) { + $info['regions_hidden'][] = $region; + } + } + } +} + +/** + * Implements hook_element_info_alter(). + */ +function theme_layout_element_info_alter(array &$info) { + if (isset($info['page'])) { + $info['page']['#pre_render'][] = 'theme_layout_page_pre_render'; + } +} + +/** + * #pre_render callback: Sets the #theme and #attached library for a layout. + */ +function theme_layout_page_pre_render($element) { + $theme = \Drupal::theme()->getActiveTheme()->getName(); + if ($layout_id = _theme_layout_get_layout_id($theme)) { + $regions = []; + foreach (Element::children($element) as $name) { + // Move the item from the top-level of $build into a region-specific + // section. + // @todo Ideally the array structure would remain unchanged, see + // https://www.drupal.org/node/2846393. + $regions[$name] = $element[$name]; + unset($element[$name]); + } + + $layout = \Drupal::service('plugin.manager.core.layout')->createInstance($layout_id); + $element['_theme_layout'] = $layout->build($regions); + unset($element['#type'], $element['#theme']); + } + return $element; +} + +/** + * Gets the layout ID for a given theme. + * + * @return string + * The layout ID for the given theme. + */ +function _theme_layout_get_layout_id($theme) { + return \Drupal::config($theme . '.settings')->get('third_party_settings.theme_layout.layout'); +} diff --git a/core/themes/seven/seven.layouts.yml b/core/themes/seven/seven.layouts.yml new file mode 100644 index 0000000..84b87da --- /dev/null +++ b/core/themes/seven/seven.layouts.yml @@ -0,0 +1,28 @@ +seven: + label: 'Seven' + theme_hook: page + library: seven/global-styling + category: 'Theme-specific' + default_region: content + regions: + header: + label: Header + pre_content: + label: Pre-Content + breadcrumb: + label: Breadcrumb + highlighted: + label: Highlighted + help: + label: Help + content: + label: Content + page_top: + label: 'Page top' + hidden: true + page_bottom: + label: 'Page bottom' + hidden: true + sidebar_first: + label: 'Sidebar first' + hidden: true