diff --git a/core/composer.json b/core/composer.json index 9ac5ab0..421c222 100644 --- a/core/composer.json +++ b/core/composer.json @@ -112,6 +112,7 @@ "drupal/migrate_drupal_ui": "self.version", "drupal/node": "self.version", "drupal/options": "self.version", + "drupal/outside_in": "self.version", "drupal/page_cache": "self.version", "drupal/path": "self.version", "drupal/quickedit": "self.version", diff --git a/core/modules/outside_in/css/outside_in.module.css b/core/modules/outside_in/css/outside_in.module.css new file mode 100644 index 0000000..a7fec82 --- /dev/null +++ b/core/modules/outside_in/css/outside_in.module.css @@ -0,0 +1,200 @@ +/** + * @file + * Styling for Outside-In module. + */ + +/* Position the offcanvas tray container outside the right of the viewport. */ +#offcanvas { + box-sizing: border-box; + height: 100%; + overflow-y: auto; + z-index: 501; +} + +/* Shift the main canvas to the right for right-to-left languages. */ +[dir="rtl"] #main-canvas-wrapper.js-tray-open #main-canvas { + right: 0; +} + +/* Position the button that closes the offcanvas tray. */ +#offcanvas > button.offcanvasClose { + position: static; + float: right; /* LTR */ + height: 52px; + width: 40px; + border: 0; + border-radius: 0; + background: url(../../../misc/icons/bebebe/ex.svg) center center no-repeat; + color: transparent; + cursor: pointer; + z-index: 501; +} +#offcanvas > button.offcanvasClose:focus { + outline: none; +} +[dir="rtl"] #offcanvas > button.offcanvasClose { + float: left; +} + +/* Create a place to name the tray. */ +#offcanvas h1 { + padding: 15px 25% 15px 15px; /* LTR */ + margin-top: 0; + margin-bottom: 0; + font-size: 120%; +} +[dir="rtl"] #offcanvas h1 { + text-align: right; + padding-right: 0; + padding-left: 25%; +} + +/* Wrap the form that's inside the offcanvas tray. */ +#offcanvas > .offcanvas-content { + height: 10000px; + padding: 0 15px; +} +[dir="rtl"] #offcanvas .offcanvas-content { + text-align: right; +} +#offcanvas > .form-item, +#offcanvas > .form-item .form-item { + width: 100%; +} + +/* + * Position the edit toolbar tab. + * @todo Move changes into toolbar module when outside-in is not experimental. + */ +.toolbar .toolbar-bar .contextual-toolbar-tab.toolbar-tab { + float: left; +} + +/* Media queries. */ +@media (max-width: 700px) { + #offcanvas { + position: absolute; + display: block; + right: 0; + top: 0; + width: 300px; + margin-right: -300px; + padding-top: 39px; + } + /* Wrap the rest of the site so we can control its width. */ + #main-canvas-wrapper #main-canvas { + display: inline-block; + width: 100%; + } + #main-canvas-wrapper.js-tray-open #offcanvas { + margin-right: 0; + right: 0; + top: 0; + } + #main-canvas-wrapper.js-tray-open #main-canvas { + position: static; + width: 100%; + } +} +@media (min-width: 700px) { + /* Position the offcanvas tray container outside the right of the viewport. */ + #offcanvas { + position: fixed; + display: inline-block; + width: 35%; + -webkit-transform: translateX(100%); + -moz-transform: translateX(100%); + -o-transform: translateX(100%); + -ms-transform: translateX(100%); + transform: translateX(100%); + } + [dir="rtl"] #offcanvas { + text-align: right; + -webkit-transform: translateX(-100%); + -moz-transform: translateX(-100%); + -o-transform: translateX(-100%); + -ms-transform: translateX(-100%); + transform: translateX(-100%); + } + /* Wrap the rest of the site so we can control its width. */ + #main-canvas-wrapper #main-canvas { + display: inline-block; + width: 100%; + } + /* Move the offcanvas tray on canvas. */ + #main-canvas-wrapper.js-tray-open #offcanvas { + -webkit-transform: translateX(0); + -moz-transform: translateX(0); + -o-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); + } + /* Reduce the width of the main canvas to provide space for the offcanvas tray. */ + #main-canvas-wrapper.js-tray-open #main-canvas { + width: 65%; + } +} +@media (min-width: 900px) { + /* Position the offcanvas tray container outside the right of the viewport. */ + #offcanvas { + position: fixed; + display: inline-block; + width: 30%; + } + /* Wrap the rest of the site so we can control its width. */ + #main-canvas-wrapper #main-canvas { + display: inline-block; + width: 100%; + } + /* Reduce the width of the main canvas to provide space for the offcanvas tray. */ + #main-canvas-wrapper.js-tray-open #main-canvas { + width: 70%; + } +} +@media (min-width: 1000px) { + /* Position the offcanvas tray container outside the right of the viewport. */ + #offcanvas { + position: fixed; + display: inline-block; + width: 25%; + } + /* Wrap the rest of the site so we can control its width. */ + #main-canvas-wrapper #main-canvas { + display: inline-block; + width: 100%; + } + /* Reduce the width of the main canvas to provide space for the offcanvas tray. */ + #main-canvas-wrapper.js-tray-open #main-canvas { + width: 75%; + } +} + +/* + * Form layout changes, mostly specific to Bartik theme and menu. + * @todo Remove when more general form styling is done. + */ +#offcanvas td { + width: auto; +} +#offcanvas .menu-enabled { + width: auto; +} +#offcanvas table#menu-overview th { + display: none; +} +#offcanvas table#menu-overview tr td:first-child { + min-width: 110px; +} +#offcanvas details > .details-wrapper { + padding: 5px; + overflow: scroll; +} +#offcanvas .tabledrag-toggle-weight { + font-size: 80%; +} +#offcanvas input:focus, +#offcanvas summary:focus { + outline: none; + box-shadow: 2px 2px #ddd; +} + diff --git a/core/modules/outside_in/css/outside_in.motion.css b/core/modules/outside_in/css/outside_in.motion.css new file mode 100644 index 0000000..f322241 --- /dev/null +++ b/core/modules/outside_in/css/outside_in.motion.css @@ -0,0 +1,68 @@ +/** + * @file + * Motion effects for outside-in module. + * + * Motion effects are in a separate file so that they can be easily turned off + * to improve performance if desired. + * + * @todo Move motion effects file into toolbar module and add a configuration + * option to performance to disable this file. + */ + +/* Transition the offcanvas tray container, with 2s delay to match main canvas speed. */ +#offcanvas { + -webkit-transition: all .7s ease 2s; + -moz-transition: all .7s ease 2s; + transition: all .7s ease 2s; +} +#main-canvas-wrapper #main-canvas, +#main-canvas-wrapper.js-tray-open #main-canvas { + -webkit-transition: all .7s ease; + -moz-transition: all .7s ease; + transition: all .7s ease; +} + +/* Transition the edit icon in the toolbar. */ +#toolbar-bar.button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before { + -webkit-transition: all .7s ease; + -moz-transition: all .7s ease; + transition: all .7s ease; +} + +/* Transition the editables on the page, their contextual links and their hover states. */ +#main-canvas-wrapper .contextual, +#main-canvas-wrapper .outside-in-editable, +#main-canvas-wrapper.js-tray-open .outside-in-editable { + -webkit-transition: all .7s ease; + -moz-transition: all .7s ease; + transition: all .7s ease; +} + +/* Transition the position of the toolbar. */ +.toolbar-fixed, +.toolbar-tray-open { + -webkit-transition: all .5s ease; + -moz-transition: all .5s ease; + transition: all .5s ease; +} + +@media (max-width: 700px) { + #offcanvas { + -webkit-transition: all .7s ease; + -moz-transition: all .7s ease; + transition: all .7s ease; + } + #main-canvas-wrapper.js-tray-open #offcanvas { + -webkit-transition: all .7s ease; + -moz-transition: all .7s ease; + transition: all .7s ease; + } +} + +/* Transition the administration tray. +#toolbar-administration, +#toolbar-administration * { + -webkit-transition: all .7s ease; + -moz-transition: all .7s ease; + transition: all .7s ease; +}*/ diff --git a/core/modules/outside_in/css/outside_in.theme.css b/core/modules/outside_in/css/outside_in.theme.css new file mode 100644 index 0000000..c063115 --- /dev/null +++ b/core/modules/outside_in/css/outside_in.theme.css @@ -0,0 +1,104 @@ +/** + * @file + * Visual styling for Outside-In module. + */ + +/* Style the edit tab in the toolbar. */ +/* @todo Move this into core when module is not experimental. */ + +/* Style both the edit and editing states. */ +button.toolbar-icon.toolbar-icon-edit.toolbar-item { + background: #0e69be; + background-image: -webkit-linear-gradient(top, #0094f0, #0e69be); + background-image: linear-gradient(to bottom, #0094f0, #0e69be); +} +button.toolbar-icon.toolbar-icon-edit.toolbar-item:hover, +button.toolbar-icon.toolbar-icon-edit.toolbar-item:focus { + background-image: -webkit-linear-gradient(top, #0094f0, #0e69be); + background-image: linear-gradient(to bottom, #0094f0, #0e69be); + color: #fff; +} +button.toolbar-icon.toolbar-icon-edit.toolbar-item:before:hover, +button.toolbar-icon.toolbar-icon-edit.toolbar-item:before:focus { + background-image: url(../../../misc/icons/ffffff/pencil.svg); +} +button.toolbar-icon.toolbar-icon-edit.toolbar-item:hover, +button.toolbar-icon.toolbar-icon-edit.toolbar-item:focus { + background-image: -webkit-linear-gradient(top, #0094f0, #0e69be); + background-image: linear-gradient(to bottom, #0094f0, #0e69be); + outline: none; +} +button.toolbar-icon.toolbar-icon-edit.toolbar-item:hover > .toolbar-icon-edit:before { + background-image: url(../../../misc/icons/ffffff/pencil.svg); +} +#toolbar-bar.button.toolbar-icon.toolbar-icon.toolbar-icon-edit:before { + background-image: url(../../../misc/icons/ffffff/pencil.svg); +} + +/* Style the toolbar when in edit mode. */ +#toolbar-bar.js-outside-in-edit-mode { + background-color: #fff; +} +/* Change text color for white background. */ +#toolbar-bar.js-outside-in-edit-mode .toolbar-item { + color: #999; +} +#toolbar-bar.js-outside-in-edit-mode .toolbar-item .is-active { + color: #333; +} +/* Set color back to white for 'editing' button only. */ +#toolbar-bar.js-outside-in-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active { + color: #fff; +} +#toolbar-bar.js-outside-in-edit-mode button.toolbar-icon.toolbar-icon-edit.toolbar-item.is-active:hover { + background-image: -webkit-linear-gradient(top, #0094f0, #0e69be); + background-image: linear-gradient(to bottom, #0094f0, #0e69be); +} + +/* + * Style the editables while in edit mode. + */ + +/* Highlight editable regions in edit mode. */ +#main-canvas.js-outside-in-edit-mode .outside-in-editable { + outline: 1px dashed rgba(0,0,0,0.5); + box-shadow: 0 0 0 1px rgba(255,255,255,0.7); +} +#main-canvas.js-outside-in-edit-mode .outside-in-editable:hover { + outline: 1px dashed rgba(0,0,0,0.5); + box-shadow: 0 0 0 1px rgba(255,255,255,0.7); + background-color: rgba(0,0,0,0.2); +} +/* Turn off the outlines on editables when the tray is open. */ +#main-canvas-wrapper.js-tray-open .outside-in-editable { + outline: transparent; + outline-color: transparent; + box-shadow: none; +} +#main-canvas-wrapper.js-tray-open .contextual { + opacity: 0; +} +#main-canvas-wrapper.js-tray-open .contextual:hover { + opacity: 1; +} + +/* + * Style the offcanvas container. + */ +div#offcanvas { + background: #fff; + border-left: 1px solid #ddd; /* LTR */ + box-shadow: -2px 2px 1px 1px rgba(0, 0, 0, 0.1); /* LTR */ +} +[dir="rtl"] div#offcanvas { + border-right: 1px solid #ddd; + box-shadow: 2px 2px 1px 1px rgba(0, 0, 0, 0.1); +} + +/* Style the tray header. */ +#offcanvas h1 { + font-size: 120%; + border-bottom: 1px solid #ddd; +} + + diff --git a/core/modules/outside_in/js/offcanvas.js b/core/modules/outside_in/js/offcanvas.js new file mode 100644 index 0000000..6904f3e --- /dev/null +++ b/core/modules/outside_in/js/offcanvas.js @@ -0,0 +1,135 @@ +/** + * @file + * Drupal's off-canvas library. + */ + +(function ($, Drupal) { + + 'use strict'; + + /** + * Create a wrapper container for the off-canvas element. + * + * @param {number} pageWidth + * The width of #page-wrapper. + * + * @return {object} + * jQuery object that is the off-canvas wrapper element. + */ + var createOffCanvasWrapper = function (pageWidth) { + return $('
', { + 'id': 'offcanvas', + 'role': 'region', + 'aria-labelledby': 'offcanvas-header' + }); + }; + + /** + * Create the title element for the off-canvas element. + * + * @param {string} title + * The title string. + * + * @return {object} + * jQuery object that is the off-canvas title element. + */ + var createTitle = function (title) { + return $('

', {text: title, id: 'offcanvas-header'}); + }; + + /** + * Create the actual off-canvas content. + * + * @param {string} data + * This is fully rendered HTML from Drupal. + * + * @return {object} + * jQuery object that is the off-canvas content element. + */ + var createOffCanvasContent = function (data) { + return $('
', {class: 'offcanvas-content', html: data}); + }; + + /** + * Create the off-canvas close element. + * + * @param {object} offCanvasWrapper + * The jQuery off-canvas wrapper element + * @param {object} pageWrapper + * The jQuery off page wrapper element + * + * @return {jQuery} + * jQuery object that is the off-canvas close element. + */ + var createOffCanvasClose = function (offCanvasWrapper, pageWrapper) { + return $('

'; + // @todo Update help text. + $output .= '

' . t('The Outside-In module is something that we should have help for. For more information, see the online documentation for the Outside-In module.', [':outside-in-documentation' => 'https://www.drupal.org/documentation/modules/outside_in']) . '

'; + return $output; + } +} + +/** + * Implements hook_contextual_links_view_alter(). + * + * Change Configure Blocks into offcanvas links. + */ +function outside_in_contextual_links_view_alter(&$element, $items) { + if (isset($element['#links']['outside-inblock-configure'])) { + $element['#links']['outside-inblock-configure']['attributes'] = [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'offcanvas', + ]; + + $element['#attached']['library'][] = 'outside_in/drupal.off_canvas'; + } +} + +/** + * Implements hook_page_top(). + */ +function outside_in_page_top(array &$page_top) { + // Opens a div for consistent wrapping to {{ page }} render in all themes. + $page_top['outside_in_tray_open'] = [ + '#markup' => '
', + '#weight' => 1000, + ]; +} + +/** + * Implements hook_page_bottom(). + */ +function outside_in_page_bottom(array &$page_bottom) { + // Closes a div for consistent wrapping to {{ page }} render in all themes. + $page_bottom['outside_in_tray_close'] = [ + '#markup' => '
', + '#weight' => -1000, + ]; +} + +/** + * Implements hook_entity_type_build(). + */ +function outside_in_entity_type_build(array &$entity_types) { + /* @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ + $entity_types['block'] + ->setFormClass('offcanvas', BlockEntityOffCanvasForm::class) + ->setLinkTemplate('offcanvas-form', '/admin/structure/block/manage/{block}/offcanvas'); +} + +/** + * Implements hook_preprocess_HOOK() for block templates. + */ +function outside_in_preprocess_block(&$variables) { + // The main system block does not contain the block contextual links. + $variables['#cache']['contexts'][] = 'outside_in.is_applied'; + if (_outside_in_apply_on_current_page() && $variables['plugin_id'] !== 'system_main_block') { + // Add class to all blocks to allow Javascript to target. + $variables['attributes']['class'][] = 'outside-in-editable'; + } +} + +/** + * Determines if the Outside-In logic should be run on the current page. + * + * @return bool + * TRUE if the Outside-In logic should be run. + */ +function _outside_in_apply_on_current_page() { + // Remove on Admin routes. + $admin_route = \Drupal::service('router.admin_context')->isAdminRoute(); + // @todo Check if there is actually a different admin theme. + // Remove on Block Demo page. + $admin_demo = \Drupal::routeMatch()->getRouteName() === 'block.admin_demo'; + return \Drupal::currentUser()->hasPermission('administer blocks') && !$admin_route && !$admin_demo; +} + +/** + * Implements hook_toolbar_alter(). + * + * Includes outside_library if Edit link is in toolbar. + */ +function outside_in_toolbar_alter(&$items) { + $items['contextual']['#cache']['contexts'][] = 'outside_in.is_applied'; + if (_outside_in_apply_on_current_page() && isset($items['contextual']['tab'])) { + $items['contextual']['#weight'] = -1000; + $items['contextual']['#attached']['library'][] = 'outside_in/drupal.outside_in'; + + // Set a class on items to mark whether they should be active in edit mode. + // @todo Create a dynamic method for modules to set their own items. + $edit_mode_items = ['contextual', 'block_place']; + foreach ($items as $key => $item) { + if (!in_array($key, $edit_mode_items) && (!isset($items[$key]['#wrapper_attributes']['class']) || !in_array('hidden', $items[$key]['#wrapper_attributes']['class']))) { + $items[$key]['#wrapper_attributes']['class'][] = 'edit-mode-inactive'; + } + } + } +} + +/** + * Implements hook_block_alter(). + */ +function outside_in_block_alter(&$definitions) { + if (!empty($definitions['system_branding_block'])) { + $definitions['system_branding_block']['forms']['offcanvas'] = SystemBrandingOffCanvasForm::class; + } + + // Since menu blocks use derivatives, check the definition ID instead of + // relying on the plugin ID. + foreach ($definitions as &$definition) { + if ($definition['id'] === 'system_menu_block') { + $definition['forms']['offcanvas'] = SystemMenuOffCanvasForm::class; + } + } +} diff --git a/core/modules/outside_in/outside_in.routing.yml b/core/modules/outside_in/outside_in.routing.yml new file mode 100644 index 0000000..5c81541 --- /dev/null +++ b/core/modules/outside_in/outside_in.routing.yml @@ -0,0 +1,7 @@ +entity.block.offcanvas_form: + path: '/admin/structure/block/manage/{block}/offcanvas' + defaults: + _entity_form: 'block.offcanvas' + _title: 'Configure block' + requirements: + _permission: 'administer blocks' diff --git a/core/modules/outside_in/outside_in.services.yml b/core/modules/outside_in/outside_in.services.yml new file mode 100644 index 0000000..5df38e2 --- /dev/null +++ b/core/modules/outside_in/outside_in.services.yml @@ -0,0 +1,11 @@ +services: + main_content_renderer.off_canvas: + class: Drupal\outside_in\Render\MainContent\OffCanvasRender + arguments: ['@title_resolver', '@renderer'] + tags: + - { name: render.main_content_renderer, format: drupal_offcanvas } + + cache_context.outside_in.is_applied: + class: \Drupal\outside_in\Cache\Context\OutsideInCacheContext + tags: + - { name: cache.context} diff --git a/core/modules/outside_in/src/Ajax/OpenOffCanvasDialogCommand.php b/core/modules/outside_in/src/Ajax/OpenOffCanvasDialogCommand.php new file mode 100644 index 0000000..d1ccb61 --- /dev/null +++ b/core/modules/outside_in/src/Ajax/OpenOffCanvasDialogCommand.php @@ -0,0 +1,54 @@ +dialogOptions['modal'] = FALSE; + return [ + 'command' => 'openOffCanvas', + 'selector' => $this->selector, + 'settings' => $this->settings, + 'data' => $this->getRenderedContent(), + 'dialogOptions' => $this->dialogOptions, + ]; + } + +} diff --git a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php new file mode 100644 index 0000000..089feae --- /dev/null +++ b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php @@ -0,0 +1,74 @@ +getRequest()->query->get('destination')) { + $query['destination'] = $destination; + } + $form['advanced_link'] = [ + '#type' => 'link', + '#title' => $this->t('Advanced options'), + '#url' => $this->entity->toUrl('edit-form', ['query' => $query]), + '#weight' => 1000, + ]; + + // Remove the ID and region elements. + unset($form['id'], $form['region']); + + return $form; + } + + /** + * {@inheritdoc} + */ + protected function buildVisibilityInterface(array $form, FormStateInterface $form_state) { + // Do not display the visibility. + return []; + } + + /** + * {@inheritdoc} + */ + protected function validateVisibility(array $form, FormStateInterface $form_state) { + // Intentionally empty. + } + + /** + * {@inheritdoc} + */ + protected function submitVisibility(array $form, FormStateInterface $form_state) { + // Intentionally empty. + } + + /** + * {@inheritdoc} + */ + protected function getPluginForm(BlockPluginInterface $block) { + if ($block instanceof PluginWithFormsInterface) { + return $this->pluginFormFactory->createInstance($block, 'offcanvas', 'configure'); + } + return $block; + } + +} diff --git a/core/modules/outside_in/src/Cache/Context/OutsideInCacheContext.php b/core/modules/outside_in/src/Cache/Context/OutsideInCacheContext.php new file mode 100644 index 0000000..c6d4c14 --- /dev/null +++ b/core/modules/outside_in/src/Cache/Context/OutsideInCacheContext.php @@ -0,0 +1,38 @@ +configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = $this->plugin->buildConfigurationForm($form, $form_state); + + // Unset links to Site Information form, we can make these changes here. + unset($form['block_branding']['use_site_name']['#description'], $form['block_branding']['use_site_slogan']['#description']); + + $site_config = $this->configFactory->getEditable('system.site'); + $form['site_information'] = [ + '#type' => 'details', + '#title' => t('Site details'), + '#open' => TRUE, + '#weight' => -100, + ]; + $form['site_information']['site_name'] = [ + '#type' => 'textfield', + '#title' => t('Site name'), + '#default_value' => $site_config->get('name'), + '#required' => TRUE, + ]; + $form['site_information']['site_slogan'] = [ + '#type' => 'textfield', + '#title' => t('Slogan'), + '#default_value' => $site_config->get('slogan'), + '#description' => t("How this is used depends on your site's theme."), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->plugin->validateConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $site_info = $form_state->getValue('site_information'); + $this->configFactory->getEditable('system.site') + ->set('name', $site_info['site_name']) + ->set('slogan', $site_info['site_slogan']) + ->save(); + $this->plugin->submitConfigurationForm($form, $form_state); + } + +} diff --git a/core/modules/outside_in/src/Form/SystemMenuOffCanvasForm.php b/core/modules/outside_in/src/Form/SystemMenuOffCanvasForm.php new file mode 100644 index 0000000..73cda59 --- /dev/null +++ b/core/modules/outside_in/src/Form/SystemMenuOffCanvasForm.php @@ -0,0 +1,137 @@ +menuStorage = $menu_storage; + $this->entityTypeManager = $entity_type_manager; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager')->getStorage('menu'), + $container->get('entity_type.manager'), + $container->get('string_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = $this->plugin->buildConfigurationForm([], $form_state); + // Move the menu levels section to the bottom. + $form['menu_levels']['#weight'] = 100; + + $form['entity_form'] = [ + '#type' => 'details', + '#title' => $this->t('Edit menu %label', array('%label' => $this->entity->label())), + '#open' => TRUE, + ]; + $form['entity_form'] += $this->getEntityForm($this->entity)->buildForm([], $form_state); + // Remove the label, ID, description, and buttons from the entity form. + unset($form['entity_form']['label'], $form['entity_form']['id'], $form['entity_form']['description'], $form['entity_form']['actions']); + // Since the overview form is further nested than expected, update the + // #parents. See \Drupal\menu_ui\MenuForm::form(). + $form_state->set('menu_overview_form_parents', ['settings', 'entity_form', 'links']); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->plugin->validateConfigurationForm($form, $form_state); + $this->getEntityForm($this->entity)->validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->plugin->submitConfigurationForm($form, $form_state); + $this->getEntityForm($this->entity)->submitForm($form, $form_state); + $this->entity->save(); + } + + /** + * Gets the entity form for this menu. + * + * @param \Drupal\system\MenuInterface $entity + * The menu entity. + * + * @return \Drupal\Core\Entity\EntityFormInterface + * The entity form. + */ + protected function getEntityForm(MenuInterface $entity) { + $entity_form = $this->entityTypeManager->getFormObject('menu', 'edit'); + $entity_form->setEntity($entity); + return $entity_form; + } + + /** + * {@inheritdoc} + */ + public function setPlugin(PluginInspectionInterface $plugin) { + $this->plugin = $plugin; + $this->entity = $this->menuStorage->load($this->plugin->getDerivativeId()); + } + +} diff --git a/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php b/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php new file mode 100644 index 0000000..5d24c09 --- /dev/null +++ b/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php @@ -0,0 +1,63 @@ +renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) { + $response = new AjaxResponse(); + + // First render the main content, because it might provide a title. + $content = $this->renderer->renderRoot($main_content); + + // Attach the library necessary for using the OpenOffCanvasDialogCommand and + // set the attachments for this Ajax response. + $main_content['#attached']['library'][] = 'outside_in/drupal.off_canvas'; + $response->setAttachments($main_content['#attached']); + + // If the main content doesn't provide a title, use the title resolver. + $title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject()); + + // Determine the title: use the title provided by the main content if any, + // otherwise get it from the routing information. + $options = $request->request->get('dialogOptions', []); + + $response->addCommand(new OpenOffCanvasDialogCommand($title, $content, $options)); + return $response; + } + +} diff --git a/core/modules/outside_in/src/Tests/Ajax/OffCanvasDialogTest.php b/core/modules/outside_in/src/Tests/Ajax/OffCanvasDialogTest.php new file mode 100644 index 0000000..2d360f9 --- /dev/null +++ b/core/modules/outside_in/src/Tests/Ajax/OffCanvasDialogTest.php @@ -0,0 +1,51 @@ +drupalLogin($this->drupalCreateUser(['administer contact forms'])); + // Ensure the elements render without notices or exceptions. + $this->drupalGet('ajax-test/dialog'); + + // Set up variables for this test. + $dialog_renderable = AjaxTestController::dialogContents(); + $dialog_contents = \Drupal::service('renderer')->renderRoot($dialog_renderable); + + $offcanvas_expected_response = [ + 'command' => 'openOffCanvas', + 'selector' => '#drupal-offcanvas', + 'settings' => NULL, + 'data' => $dialog_contents, + 'dialogOptions' => [ + 'modal' => FALSE, + 'title' => 'AJAX Dialog contents', + ], + ]; + + // Emulate going to the JS version of the page and check the JSON response. + $ajax_result = $this->drupalGetAjax('ajax-test/dialog-contents', ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_offcanvas']]); + $this->assertEqual($offcanvas_expected_response, $ajax_result[3], 'Off-canvas dialog JSON response matches.'); + } + +} diff --git a/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.info.yml b/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.info.yml new file mode 100644 index 0000000..8c6cc80 --- /dev/null +++ b/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.info.yml @@ -0,0 +1,9 @@ +name: 'Off-canvas tests' +type: module +description: 'Provides off-canvas test links.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - block + - outside_in diff --git a/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.routing.yml b/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.routing.yml new file mode 100644 index 0000000..7bfd52b --- /dev/null +++ b/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.routing.yml @@ -0,0 +1,23 @@ +offcanvas_test.links: + path: '/offcanvas-test-links' + defaults: + _controller: '\Drupal\offcanvas_test\Controller\TestController::linksDisplay' + _title: 'Links' + requirements: + _access: 'TRUE' + +offcanvas_test.thing1: + path: '/offcanvas-thing1' + defaults: + _controller: '\Drupal\offcanvas_test\Controller\TestController::thing1' + _title: 'Thing 1' + requirements: + _access: 'TRUE' + +offcanvas_test.thing2: + path: '/offcanvas-thing2' + defaults: + _controller: '\Drupal\offcanvas_test\Controller\TestController::thing2' + _title: 'Thing 2' + requirements: + _access: 'TRUE' diff --git a/core/modules/outside_in/tests/modules/offcanvas_test/src/Controller/TestController.php b/core/modules/outside_in/tests/modules/offcanvas_test/src/Controller/TestController.php new file mode 100644 index 0000000..6295f55 --- /dev/null +++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Controller/TestController.php @@ -0,0 +1,78 @@ + 'markup', + '#markup' => 'Thing 1 says hello', + ]; + } + + /** + * Thing2. + * + * @return string + * Return Hello string. + */ + public function thing2() { + return [ + '#type' => 'markup', + '#markup' => 'Thing 2 says hello', + ]; + } + + /** + * Displays test links that will open in offcanvas tray. + * + * @return array + * Render array with links. + */ + public function linksDisplay() { + return [ + 'offcanvas_link_1' => [ + '#title' => 'Click Me 1!', + '#type' => 'link', + '#url' => Url::fromRoute('offcanvas_test.thing1'), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'offcanvas', + ], + '#attached' => [ + 'library' => [ + 'outside_in/drupal.off_canvas', + ], + ], + ], + 'offcanvas_link_2' => [ + '#title' => 'Click Me 2!', + '#type' => 'link', + '#url' => Url::fromRoute('offcanvas_test.thing2'), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'offcanvas', + ], + '#attached' => [ + 'library' => [ + 'outside_in/drupal.off_canvas', + ], + ], + ], + + ]; + } + +} diff --git a/core/modules/outside_in/tests/modules/offcanvas_test/src/Plugin/Block/TestBlock.php b/core/modules/outside_in/tests/modules/offcanvas_test/src/Plugin/Block/TestBlock.php new file mode 100644 index 0000000..da72f9b --- /dev/null +++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Plugin/Block/TestBlock.php @@ -0,0 +1,49 @@ + [ + '#title' => $this->t('Click Me 1!'), + '#type' => 'link', + '#url' => Url::fromRoute('offcanvas_test.thing1'), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'offcanvas', + ], + ], + 'offcanvas_link_2' => [ + '#title' => $this->t('Click Me 2!'), + '#type' => 'link', + '#url' => Url::fromRoute('offcanvas_test.thing2'), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'offcanvas', + ], + ], + '#attached' => [ + 'library' => [ + 'outside_in/drupal.off_canvas', + ], + ], + ]; + } + +} diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php new file mode 100644 index 0000000..12b7da6 --- /dev/null +++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php @@ -0,0 +1,61 @@ +enableTheme($theme); + $this->drupalGet('/offcanvas-test-links'); + + $page = $this->getSession()->getPage(); + $web_assert = $this->assertSession(); + + // Make sure off-canvas tray is on page when first loaded. + $web_assert->elementNotExists('css', '#offcanvas'); + + // Check opening and closing with two separate links. + // Make sure tray updates to new content. + foreach (['1', '2'] as $link_index) { + // Click the first test like that should open the page. + $page->clickLink("Click Me $link_index!"); + $this->waitForOffCanvasToOpen(); + + // Check that the canvas is not on the page. + $web_assert->elementExists('css', '#offcanvas'); + // Check that response text is on page. + $web_assert->pageTextContains("Thing $link_index says hello"); + $offcanvas_tray = $this->getTray(); + + // Check that tray is visible. + $this->assertEquals(TRUE, $offcanvas_tray->isVisible()); + $header_text = $offcanvas_tray->findById('offcanvas-header')->getText(); + + // Check that header is correct. + $this->assertEquals("Thing $link_index", $header_text); + $tray_text = $offcanvas_tray->find('css', '.offcanvas-content')->getText(); + $this->assertEquals("Thing $link_index says hello", $tray_text); + } + } + } + +} diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php new file mode 100644 index 0000000..7f74180 --- /dev/null +++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php @@ -0,0 +1,111 @@ +enableTheme('bartik'); + $user = $this->createUser([ + 'administer blocks', + 'access contextual links', + 'access toolbar', + ]); + $this->drupalLogin($user); + + $this->placeBlock('system_powered_by_block', ['id' => 'powered']); + $this->placeBlock('system_branding_block', ['id' => 'branding']); + } + + /** + * Tests updating the "Powered by Drupal" block in the Off-Canvas tray. + */ + public function testPoweredByBlock() { + + $page = $this->getSession()->getPage(); + $web_assert = $this->assertSession(); + + $this->drupalGet('user'); + $this->enableEditingMode(); + + // Open "Powered by Drupal" block form by clicking div. + $page->find('css', '#block-powered')->click(); + $this->waitForOffCanvasToOpen(); + $this->assertOffCanvasBlockFormIsValid(); + + // Fill out form, save the form. + $new_label = 'Can you imagine anyone showing the label on this block?'; + $page->fillField('settings[label]', $new_label); + $page->checkField('settings[label_display]'); + $this->getTray()->pressButton('Save block'); + // Make sure the changes are present. + $web_assert->pageTextContains($new_label); + } + + /** + * Tests updating the System Branding block in the Off-Canvas tray. + * + * Also tests updating the site name. + */ + public function testBrandingBlock() { + $web_assert = $this->assertSession(); + $this->drupalGet('user'); + $page = $this->getSession()->getPage(); + $this->enableEditingMode(); + + // Open branding block form by clicking div. + $page->find('css', '#block-branding')->click(); + $this->waitForOffCanvasToOpen(); + $this->assertOffCanvasBlockFormIsValid(); + + // Fill out form, save the form. + $new_site_name = 'The site that will live a very short life.'; + $page->fillField('settings[site_information][site_name]', $new_site_name); + $this->getTray()->pressButton('Save block'); + + // Make sure the changes are present. + $web_assert->pageTextContains($new_site_name); + } + + /** + * Enables Editing mode by pressing "Edit" button in the toolbar. + */ + protected function enableEditingMode() { + $this->waitForElement('div[data-contextual-id="block:block=powered:langcode=en"] .contextual-links a'); + + $this->waitForElement('#toolbar-bar', 3000); + + $edit_button = $this->getSession()->getPage()->find('css', '#toolbar-bar div.contextual-toolbar-tab button'); + + $edit_button->press(); + } + + /** + * Asserts that Off-Canvas block form is valid. + */ + protected function assertOffCanvasBlockFormIsValid() { + $web_assert = $this->assertSession(); + // Check that common block form elements exist. + $web_assert->elementExists('css', 'input[data-drupal-selector="edit-settings-label"]'); + $web_assert->elementExists('css', 'input[data-drupal-selector="edit-settings-label-display"]'); + // Check that advanced block form elements do not exist. + $web_assert->elementNotExists('css', 'input[data-drupal-selector="edit-visibility-request-path-pages"]'); + $web_assert->elementNotExists('css', 'select[data-drupal-selector="edit-region"]'); + } + +} diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php new file mode 100644 index 0000000..5871aef --- /dev/null +++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInJavascriptTestBase.php @@ -0,0 +1,63 @@ +install([$theme]); + $theme_config = \Drupal::configFactory()->getEditable('system.theme'); + $theme_config->set('default', $theme); + $theme_config->save(); + } + + /** + * Waits for Off-canvas tray to open. + */ + protected function waitForOffCanvasToOpen() { + $this->waitForElement('#offcanvas'); + } + + /** + * Waits for Off-canvas tray to close. + */ + protected function waitForOffCanvasToClose() { + $condition = "(jQuery('#offcanvas').length == 0)"; + $this->assertJsCondition($condition); + } + + /** + * Waits for an element to appear on the page. + * + * @param string $selector + * CSS selector. + * @param int $timeout + * (optional) Timeout in milliseconds, defaults to 1000. + */ + protected function waitForElement($selector, $timeout = 1000) { + $condition = "(jQuery('$selector').length > 0)"; + $this->assertJsCondition($condition, $timeout); + } + + /** + * Gets the Off-Canvas tray element. + * + * @return \Behat\Mink\Element\NodeElement|null + */ + protected function getTray() { + return $this->getSession()->getPage()->findById('offcanvas'); + } + +} diff --git a/core/modules/outside_in/tests/src/Unit/Ajax/OpenOffCanvasDialogCommandTest.php b/core/modules/outside_in/tests/src/Unit/Ajax/OpenOffCanvasDialogCommandTest.php new file mode 100644 index 0000000..45ff2a1 --- /dev/null +++ b/core/modules/outside_in/tests/src/Unit/Ajax/OpenOffCanvasDialogCommandTest.php @@ -0,0 +1,34 @@ +Text!

', ['url' => 'example']); + + $expected = [ + 'command' => 'openOffCanvas', + 'selector' => '#drupal-offcanvas', + 'settings' => NULL, + 'data' => '

Text!

', + 'dialogOptions' => [ + 'url' => 'example', + 'title' => 'Title', + 'modal' => FALSE, + ], + ]; + $this->assertEquals($expected, $command->render()); + } + +}