diff --git a/core/modules/layout/layout.info b/core/modules/layout/layout.info new file mode 100644 index 00000000000..187bfea6e0b --- /dev/null +++ b/core/modules/layout/layout.info @@ -0,0 +1,5 @@ +name = Layout +description = Makes it possible to swap different page layouts. +package = Core +version = VERSION +core = 8.x diff --git a/core/modules/layout/layout.module b/core/modules/layout/layout.module new file mode 100644 index 00000000000..7d8279826c9 --- /dev/null +++ b/core/modules/layout/layout.module @@ -0,0 +1,33 @@ +<?php + +/** + * @file + * Manages page layouts for content presentation. + */ + +/** + * Get the layout plugin manager instance. + * + * @return Drupal\layout\Plugin\Type\LayoutManager + * The layout plugin manager instance. + */ +function layout_manager() { + return drupal_container()->get('plugin.manager.layout'); +} + +/** + * Implements hook_theme(). + * + * Expose all layouts as theme items, so themes can override layout markup. + */ +function layout_theme($existing, $type, $theme, $path) { + $items = array(); + foreach (layout_manager()->getDefinitions() as $name => $layout) { + $items[$layout['theme']] = array( + 'variables' => array('content' => NULL), + 'path' => $layout['path'], + 'template' => $layout['template'], + ); + } + return $items; +} diff --git a/core/modules/layout/lib/Drupal/layout/LayoutBundle.php b/core/modules/layout/lib/Drupal/layout/LayoutBundle.php new file mode 100644 index 00000000000..59b8513027e --- /dev/null +++ b/core/modules/layout/lib/Drupal/layout/LayoutBundle.php @@ -0,0 +1,25 @@ +<?php + +/** + * @file + * Definition of Drupal\layout\LayoutBundle. + */ + +namespace Drupal\Layout; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; + +/** + * Layout dependency injection container. + */ +class LayoutBundle extends Bundle { + + /** + * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build(). + */ + public function build(ContainerBuilder $container) { + // Register the LayoutManager class with the dependency injection container. + $container->register('plugin.manager.layout', 'Drupal\layout\Plugin\Type\LayoutManager'); + } +} diff --git a/core/modules/layout/lib/Drupal/layout/Plugin/Derivative/Layout.php b/core/modules/layout/lib/Drupal/layout/Plugin/Derivative/Layout.php new file mode 100644 index 00000000000..ce82c655a31 --- /dev/null +++ b/core/modules/layout/lib/Drupal/layout/Plugin/Derivative/Layout.php @@ -0,0 +1,117 @@ +<?php + +/** + * @file + * Definition of Drupal\layout\Plugin\Derivative\Layout. + */ + +namespace Drupal\layout\Plugin\Derivative; + +use DirectoryIterator; +use Drupal\Component\Plugin\Derivative\DerivativeInterface; +use Drupal\Core\Config\FileStorage; + +/** + * Layout plugin derivative definition. + */ +class Layout implements DerivativeInterface { + + /** + * List of derivatives. + * + * Associative array keyed by 'provider__layoutname' where provider is the + * module or theme name and layoutname is the .yml filename, such as + * 'bartik__page' or 'layout__one-col'. The values of the array are + * associative arrays themselves with metadata about the layout such as + * 'template', 'css', 'admin css' and so on. + * + * @var array + */ + protected $derivatives = array(); + + /** + * Layout derivative type. + * + * Defines the subdirectory under ./layout where layout metadata is loooked + * for. Overriding implementations should change this to look for other + * types in a different subdirectory. + * + * @var string + */ + protected $type = 'static'; + + /** + * Implements DerivativeInterface::getDerivativeDefinition(). + */ + public function getDerivativeDefinition($derivative_id, array $base_plugin_definition) { + if (!empty($this->derivatives) && !empty($this->derivatives[$derivative_id])) { + return $this->derivatives[$derivative_id]; + } + $this->getDerivativeDefinitions($base_plugin_definition); + return $this->derivatives[$derivative_id]; + } + + /** + * Implements DerivativeInterface::getDerivativeDefinitions(). + */ + public function getDerivativeDefinitions(array $base_plugin_definition) { + $available_layout_providers = array(); + + // Add all modules as possible layout providers. + foreach (module_list() as $module) { + $available_layout_providers[$module] = array( + 'type' => 'module', + 'provider' => $module, + 'dir' => drupal_get_path('module', $module), + ); + } + + // Add all themes as possible layout providers. + foreach (list_themes() as $theme_id => $theme) { + $available_layout_providers[$theme_id] = array( + 'type' => 'theme', + 'provider' => $theme->name, + 'dir' => drupal_get_path('theme', $theme->name), + ); + } + + foreach ($available_layout_providers as $provider) { + // Looks for layouts in the 'layout' directory under the module/theme. + // There could be subdirectories under there with one layout defined + // in each. + $dir = $provider['dir'] . DIRECTORY_SEPARATOR . 'layouts' . DIRECTORY_SEPARATOR . $this->type; + if (file_exists($dir)) { + $this->iterateDirectories($dir, $provider); + } + } + return $this->derivatives; + } + + /** + * Finds layout definitions by looking for layout metadata. + */ + protected function iterateDirectories($dir, $provider) { + $directories = new DirectoryIterator($dir); + foreach ($directories as $fileinfo) { + if ($fileinfo->isDir() && !$fileinfo->isDot()) { + // Keep discovering in subdirectories to arbitrary depth. + $this->iterateDirectories($fileinfo->getPathname(), $provider); + } + elseif ($fileinfo->isFile() && pathinfo($fileinfo->getFilename(), PATHINFO_EXTENSION) == 'yml') { + // Declarative layout definitions are defined with a .yml file in a + // layout subdirectory. This provides all information about the layout + // such as layout markup template and CSS and JavaScript files to use. + $directory = new FileStorage($fileinfo->getPath()); + $key = $provider['provider'] . '__' . $fileinfo->getBasename('.yml'); + $this->derivatives[$key] = $directory->read($fileinfo->getBasename('.yml')); + $this->derivatives[$key]['theme'] = $key; + $this->derivatives[$key]['path'] = $fileinfo->getPath(); + // If the layout author didn't specify a template name, assume the same + // name as the yml file. + if (!isset($this->derivatives[$key]['template'])) { + $this->derivatives[$key]['template'] = $fileinfo->getBasename('.yml'); + } + } + } + } +} diff --git a/core/modules/layout/lib/Drupal/layout/Plugin/LayoutInterface.php b/core/modules/layout/lib/Drupal/layout/Plugin/LayoutInterface.php new file mode 100644 index 00000000000..5b874f6f4d4 --- /dev/null +++ b/core/modules/layout/lib/Drupal/layout/Plugin/LayoutInterface.php @@ -0,0 +1,30 @@ +<?php + +/** + * @file + * Definition of Drupal\layout\Plugin\LayoutInterface. + */ + +namespace Drupal\layout\Plugin; + +/** + * Defines the shared interface for all layout plugins. + */ +interface LayoutInterface { + + /** + * Returns a list of regions. + * + * @return array + * An array of region machine names. + */ + public function getRegions(); + + /** + * Renders layout and returns the rendered markup. + * + * @return string + * Rendered HTML output from the layout. + */ + public function renderLayout(); +} diff --git a/core/modules/layout/lib/Drupal/layout/Plugin/Type/LayoutManager.php b/core/modules/layout/lib/Drupal/layout/Plugin/Type/LayoutManager.php new file mode 100644 index 00000000000..e58ad4ba19e --- /dev/null +++ b/core/modules/layout/lib/Drupal/layout/Plugin/Type/LayoutManager.php @@ -0,0 +1,32 @@ +<?php + +/** + * @file + * Definition of Drupal\layout\Plugin\Type\LayoutManager. + */ + +namespace Drupal\layout\Plugin\Type; + +use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator; +use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery; +use Drupal\Component\Plugin\Factory\ReflectionFactory; + +/** + * Layout plugin manager. + */ +class LayoutManager extends PluginManagerBase { + + protected $defaults = array( + 'class' => 'Drupal\layout\Plugin\layout\layout\StaticLayout', + ); + + /** + * Overrides Drupal\Component\Plugin\PluginManagerBase::__construct(). + */ + public function __construct() { + // Create layout plugin derivatives from declaratively defined layouts. + $this->discovery = new DerivativeDiscoveryDecorator(new AnnotatedClassDiscovery('layout', 'layout')); + $this->factory = new ReflectionFactory($this); + } +} diff --git a/core/modules/layout/lib/Drupal/layout/Plugin/layout/layout/StaticLayout.php b/core/modules/layout/lib/Drupal/layout/Plugin/layout/layout/StaticLayout.php new file mode 100644 index 00000000000..4819595a521 --- /dev/null +++ b/core/modules/layout/lib/Drupal/layout/Plugin/layout/layout/StaticLayout.php @@ -0,0 +1,121 @@ +<?php + +/** + * @file + * Definition of Drupal\layout\Plugin\layout\layout\StaticLayout. + */ + +namespace Drupal\layout\Plugin\layout\layout; + +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; +use Drupal\layout\Plugin\LayoutInterface; +use Drupal\Component\Plugin\PluginBase; +use Drupal\Core\Annotation\Plugin; + +/** + * @Plugin( + * id = "static_layout", + * derivative = "Drupal\layout\Plugin\Derivative\Layout" + * ) + */ +class StaticLayout extends PluginBase implements LayoutInterface { + + /** + * Overrides Drupal\Component\Plugin\PluginBase::__construct(). + */ + public function __construct(array $configuration, $plugin_id, DiscoveryInterface $discovery) { + // Get definition by discovering the declarative information. + $definition = $discovery->getDefinition($plugin_id); + foreach ($definition['regions'] as $region => $title) { + if (!isset($configuration['regions'][$region])) { + $configuration['regions'][$region] = array(); + } + } + parent::__construct($configuration, $plugin_id, $discovery); + } + + /** + * Implements Drupal\layout\Plugin\LayoutInterface::getRegions(). + */ + public function getRegions() { + $definition = $this->getDefinition(); + return $definition['regions']; + } + + /** + * Returns the list of CSS files associated with this layout. + */ + public function getStylesheetFiles() { + $definition = $this->getDefinition(); + return isset($definition['stylesheets']) ? $definition['stylesheets'] : array(); + } + + /** + * Returns the list of administrative CSS files associated with this layout. + */ + public function getAdminStylesheetFiles() { + $definition = $this->getDefinition(); + // Fall back on regular CSS for the admin page if admin CSS not provided. + return isset($definition['admin stylesheets']) ? $definition['admin stylesheets'] : $this->getStylesheetFiles(); + } + + /** + * Returns the list of JS files associated with this layout. + */ + public function getScriptFiles() { + $definition = $this->getDefinition(); + return isset($definition['scripts']) ? $definition['scripts'] : array(); + } + + /** + * Returns the list of administrative JS files associated with this layout. + */ + public function getAdminScriptFiles() { + $definition = $this->getDefinition(); + return isset($definition['admin scripts']) ? $definition['admin scripts'] : $this->getScriptFiles(); + } + + /** + * Implements Drupal\layout\Plugin\LayoutInterface::renderLayout(). + */ + public function renderLayout($admin = FALSE) { + $definition = $this->getDefinition(); + + // Assemble a render array with the regions and attached CSS/JS. + $build = array( + '#theme' => $definition['theme'], + '#content' => array(), + ); + + // Render all regions needed for this layout. + foreach ($this->getRegions() as $region => $title) { + // @todo This is just stub code to fill in regions with stuff for now. + // When blocks are related to layouts and not themes, we can make this + // really be filled in with blocks. + $build['#content'][$region] = '<h3>' . $title . '</h3>'; + } + + // Fill in attached CSS and JS files based on metadata. + if (!$admin) { + $build['#attached'] = array( + 'css' => $this->getStylesheetFiles(), + 'js' => $this->getScriptFiles(), + ); + } + else { + $build['#attached'] = array( + 'css' => $this->getAdminStylesheetFiles(), + 'js' => $this->getAdminScriptFiles(), + ); + } + + // Include the path of the definition in all CSS and JS files. + foreach (array('css', 'js') as $type) { + foreach ($build['#attached'][$type] as &$filename) { + $filename = $definition['path'] . '/' . $filename; + } + } + + return drupal_render($build); + } +} diff --git a/core/modules/layout/lib/Drupal/layout/Tests/LayoutDerivativesTest.php b/core/modules/layout/lib/Drupal/layout/Tests/LayoutDerivativesTest.php new file mode 100644 index 00000000000..1071713eb5c --- /dev/null +++ b/core/modules/layout/lib/Drupal/layout/Tests/LayoutDerivativesTest.php @@ -0,0 +1,91 @@ +<?php + +/** + * @file + * Definition of Drupal\layout\Tests\LayoutDerivativesTest. + */ + +namespace Drupal\layout\Tests; + +use Drupal\simpletest\WebTestBase; + +/** + * Tests the layout system derivatives. + */ +class LayoutDerivativesTest extends WebTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('layout', 'layout_test'); + + public static function getInfo() { + return array( + 'name' => 'Layout derivatives', + 'description' => 'Tests layout derivatives discovery.', + 'group' => 'Layout', + ); + } + + /** + * Tests for module/theme layout derivatives. + */ + function testDerivatives() { + $manager = drupal_container()->get('plugin.manager.layout'); + + $definitions = $manager->getDefinitions(); + $this->assertTrue(is_array($definitions), 'Definitions found.'); + $this->assertTrue(count($definitions) == 2, 'Two definitions available.'); + $this->assertTrue(isset($definitions['static_layout:layout_test__one-col']), 'One column layout found.'); + $this->assertTrue(isset($definitions['static_layout:layout_test_theme__two-col']), 'Two column layout found.'); + + // Get a one column layout instance. This is defined under the layout_test + // module. + $layout = $manager->createInstance('static_layout:layout_test__one-col', array()); + // Verify the expected regions are properly available. + $regions = $layout->getRegions(); + $this->assertTrue(is_array($regions), 'Regions array present.'); + $this->assertTrue(count($regions) == 1, 'One region defined.'); + $this->assertTrue(isset($regions['middle']), 'Middle region found.'); + + // Render the layout and look at whether expected region names and classes + // were in the output. + $render = $layout->renderLayout(); + $this->drupalSetContent($render); + $this->assertText('Middle column'); + $this->assertRaw('class="layout-display layout-one-col'); + + // Get the two column page layout defined by the layout test theme. + $layout = $manager->createInstance('static_layout:layout_test_theme__two-col', array()); + // Verify the expected regions are properly available. + $regions = $layout->getRegions(); + $this->assertTrue(is_array($regions), 'Regions array present.'); + $this->assertTrue(count($regions) == 2, 'Two regions defined.'); + $this->assertTrue(isset($regions['left']), 'Left region found.'); + $this->assertTrue(isset($regions['right']), 'Right region found.'); + + // Render the layout and look at whether expected region names and classes + // were in the output. + $render = $layout->renderLayout(); + $this->drupalSetContent($render); + $this->assertText('Left side'); + $this->assertText('Right side'); + $this->assertRaw('<div class="layout-region layout-col-right">'); + } + + /** + * Test layout functionality as applies to pages. + */ + function testPageLayout() { + // The layout-test page uses the layout_test_theme page layout. + $this->drupalGet('layout-test'); + $this->assertText('Left side'); + $this->assertText('Right side'); + $this->assertRaw('<div class="layout-region layout-col-right">'); + + // Ensure the CSS was added. + $this->assertRaw('@import url("' . url('', array('absolute' => TRUE)) . drupal_get_path('theme', 'layout_test_theme') . '/layouts/static/two-col/two-col.css'); + } +} diff --git a/core/modules/layout/tests/layout_test.info b/core/modules/layout/tests/layout_test.info new file mode 100644 index 00000000000..7b054a90ed5 --- /dev/null +++ b/core/modules/layout/tests/layout_test.info @@ -0,0 +1,6 @@ +name = Layout test +description = Helps with testing layouts. +package = Testing +version = VERSION +core = 8.x +hidden = TRUE diff --git a/core/modules/layout/tests/layout_test.module b/core/modules/layout/tests/layout_test.module new file mode 100644 index 00000000000..36c3915f205 --- /dev/null +++ b/core/modules/layout/tests/layout_test.module @@ -0,0 +1,39 @@ +<?php + +/** + * @file + * Layout testing module. + */ + +/** + * Implementation of hook_menu(). + */ +function layout_test_menu() { + $items['layout-test'] = array( + 'title' => 'Layout test', + 'page callback' => 'layout_test_page', + 'access callback' => TRUE, + ); + return $items; +} + +/** + * Page callback for layout testing. + */ +function layout_test_page() { + // Hack to enable and apply the theme to this page and manually invoke its + // layout plugin and render it. + global $theme; + $theme = 'layout_test_theme'; + theme_enable(array($theme)); + $layout = layout_manager()->createInstance('static_layout:layout_test_theme__two-col'); + return $layout->renderLayout(); +} + +/** + * Implements hook_system_theme_info(). + */ +function layout_test_system_theme_info() { + $themes['layout_test_theme'] = drupal_get_path('module', 'layout_test') . '/themes/layout_test_theme/layout_test_theme.info'; + return $themes; +} diff --git a/core/modules/layout/tests/layouts/static/one-col/one-col.tpl.php b/core/modules/layout/tests/layouts/static/one-col/one-col.tpl.php new file mode 100644 index 00000000000..e47f83ec473 --- /dev/null +++ b/core/modules/layout/tests/layouts/static/one-col/one-col.tpl.php @@ -0,0 +1,18 @@ +<?php +/** + * @file + * Template for a one column layout. + * + * This template provides a very simple "one column" display layout. + * + * Variables: + * - $content: An array of content, each item in the array is keyed to one + * region of the layout. This layout supports the following sections: + * $content['middle']: The only region in the layout. + */ +?> +<div class="layout-display layout-one-col clearfix"> + <div class="layout-region layout-col"> + <div class="inside"><?php print $content['middle']; ?></div> + </div> +</div> diff --git a/core/modules/layout/tests/layouts/static/one-col/one-col.yml b/core/modules/layout/tests/layouts/static/one-col/one-col.yml new file mode 100644 index 00000000000..27d7d0601bc --- /dev/null +++ b/core/modules/layout/tests/layouts/static/one-col/one-col.yml @@ -0,0 +1,5 @@ +title: Single column +category: Columns: 1 +template: one-col +regions: + middle: 'Middle column' diff --git a/core/modules/layout/tests/themes/layout_test_theme/layout_test_theme.info b/core/modules/layout/tests/themes/layout_test_theme/layout_test_theme.info new file mode 100644 index 00000000000..84bcff06809 --- /dev/null +++ b/core/modules/layout/tests/themes/layout_test_theme/layout_test_theme.info @@ -0,0 +1,4 @@ +name = Layout test theme +description = Theme for testing the layout system +core = 8.x +hidden = TRUE diff --git a/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.css b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.css new file mode 100644 index 00000000000..6044e2dce44 --- /dev/null +++ b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.css @@ -0,0 +1,17 @@ +.layout-two-col .layout-col-left { + float: left; + width: 50%; +} + +.layout-two-col .layout-col-left .inside { + margin-right: .5em; +} + +.layout-two-col .layout-col-right { + float: right; + width: 50%; +} + +.layout-two-col .layout-col-right .inside { + margin-left: .5em; +} diff --git a/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.tpl.php b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.tpl.php new file mode 100644 index 00000000000..1d8dbe46b33 --- /dev/null +++ b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.tpl.php @@ -0,0 +1,24 @@ +<?php +/** + * @file + * Template for a 2 column layout. + * + * This template provides a two column display layout, with each column equal in + * width. + * + * Variables: + * - $content: An array of content, each item in the array is keyed to one + * region of the layout. This layout supports the following sections: + * - $content['left']: Content in the left column. + * - $content['right']: Content in the right column. + */ +?> +<div class="layout-display layout-two-col clearfix"> + <div class="layout-region layout-col-left"> + <div class="inside"><?php print $content['left']; ?></div> + </div> + + <div class="layout-region layout-col-right"> + <div class="inside"><?php print $content['right']; ?></div> + </div> +</div> diff --git a/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.yml b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.yml new file mode 100644 index 00000000000..7ee126f4c30 --- /dev/null +++ b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.yml @@ -0,0 +1,8 @@ +title: Two column +category: Columns: 2 +template: two-col +stylesheets: + - two-col.css +regions: + left: 'Left side' + right: 'Right side'