diff --git a/core/modules/outside_in/js/outside_in.js b/core/modules/outside_in/js/outside_in.js index 0b8bc88..7eb241c 100644 --- a/core/modules/outside_in/js/outside_in.js +++ b/core/modules/outside_in/js/outside_in.js @@ -3,7 +3,7 @@ * Drupal's Settings Tray library. */ -(function ($, Drupal) { +(function ($, Drupal, drupalSettings) { 'use strict'; @@ -211,8 +211,10 @@ $(toggleEditSelector).once('outsidein').on('click.outsidein', toggleEditMode); - var search = Drupal.ajax.WRAPPER_FORMAT + '=drupal_dialog'; - var replace = Drupal.ajax.WRAPPER_FORMAT + '=drupal_dialog_offcanvas'; + var searchLink = Drupal.ajax.WRAPPER_FORMAT + '=drupal_dialog'; + var searchFrom = Drupal.ajax.WRAPPER_FORMAT + '=drupal_ajax'; + var replaceLink = Drupal.ajax.WRAPPER_FORMAT + '=drupal_dialog_offcanvas'; + var replaceForm = Drupal.ajax.WRAPPER_FORMAT + '=drupal_off_canvas_ajax'; // Loop through all Ajax links and change the format to dialog-offcanvas when // needed. Drupal.ajax.instances @@ -222,21 +224,41 @@ var wrapperOffcanvas = false; if (hasElement) { rendererOffcanvas = $(instance.element).attr('data-dialog-renderer') === 'offcanvas'; - wrapperOffcanvas = instance.options.url.indexOf('drupal_dialog_offcanvas') === -1; + if (rendererOffcanvas) { + wrapperOffcanvas = instance.options.url.indexOf(replaceLink) === -1; + } + else { + if (instance.$form[0] && instance.$form[0].hasAttribute('data-off-canvas-form')) { + rendererOffcanvas = true; + wrapperOffcanvas = instance.options.url.indexOf(replaceForm) === -1; + } + } + } return hasElement && rendererOffcanvas && wrapperOffcanvas; }) .forEach(function (instance) { // @todo Move logic for data-dialog-renderer attribute into ajax.js // https://www.drupal.org/node/2784443 - instance.options.url = instance.options.url.replace(search, replace); - // Check to make sure existing dialogOptions aren't overridden. - if (!('dialogOptions' in instance.options.data)) { - instance.options.data.dialogOptions = {}; + if (instance.hasOwnProperty('dialogType')) { + instance.options.url = instance.options.url.replace(searchLink, replaceLink); + // Check to make sure existing dialogOptions aren't overridden. + if (!instance.options.data.hasOwnProperty('dialogOptions')) { + instance.options.data.dialogOptions = {}; + } + instance.options.data.dialogOptions.outsideInActiveEditableId = $(instance.element).parents('.outside-in-editable').attr('id'); + instance.options.url += '&editable_id=' + instance.options.data.dialogOptions.outsideInActiveEditableId; + instance.progress = {type: 'fullscreen'}; + } + else { + instance.options.url = instance.options.url.replace(searchFrom, replaceForm); + instance.options.data.formOptions = {messagesSelector: '.ui-dialog-offcanvas .messages__wrapper'}; } - instance.options.data.dialogOptions.outsideInActiveEditableId = $(instance.element).parents('.outside-in-editable').attr('id'); - instance.progress = {type: 'fullscreen'}; }); + var qs = drupalSettings.path.currentQuery; + if (qs && qs.hasOwnProperty('editable_id')) { + $('#' + qs.editable_id + ' a' + blockConfigureSelector).once('outside_in_qs').trigger('click'); + } } }; @@ -259,4 +281,4 @@ } }); -})(jQuery, Drupal); +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/outside_in/outside_in.libraries.yml b/core/modules/outside_in/outside_in.libraries.yml index 5eddea4..faa314a 100644 --- a/core/modules/outside_in/outside_in.libraries.yml +++ b/core/modules/outside_in/outside_in.libraries.yml @@ -20,6 +20,8 @@ drupal.outside_in: - core/drupal - core/jquery.once - core/drupal.ajax + - core/drupalSettings + - core/jquery.form drupal.off_canvas: version: VERSION js: diff --git a/core/modules/outside_in/outside_in.services.yml b/core/modules/outside_in/outside_in.services.yml index 48f5824..41879c4 100644 --- a/core/modules/outside_in/outside_in.services.yml +++ b/core/modules/outside_in/outside_in.services.yml @@ -5,6 +5,12 @@ services: tags: - { name: render.main_content_renderer, format: drupal_dialog_offcanvas } + main_content_renderer.off_canvas_ajax: + class: Drupal\outside_in\Render\MainContent\AjaxRenderer + arguments: ['@element_info'] + tags: + - { name: render.main_content_renderer, format: drupal_off_canvas_ajax } + outside_in.manager: class: Drupal\outside_in\OutsideInManager arguments: ['@router.admin_context', '@current_route_match', '@current_user'] diff --git a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php index a77f51f..08f5dad 100644 --- a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php +++ b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php @@ -36,6 +36,7 @@ public function title(BlockInterface $block) { */ public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); + $form['#attributes']['data-off-canvas-form'] = TRUE; // Create link to full block form. $query = []; @@ -97,4 +98,23 @@ protected function getPluginForm(BlockPluginInterface $block) { return $block; } + /** + * {@inheritdoc} + */ + protected function actionsElement(array $form, FormStateInterface $form_state) { + $actions_element = parent::actionsElement($form, $form_state); + $actions_element['submit']['#attributes']['class'][] = 'use-ajax-submit'; + return $actions_element; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + // \Drupal\block\BlockForm::submitForm() always redirects to block listing. + // This would doesn't work with Ajax submit. + $form_state->disableRedirect(); + } + } diff --git a/core/modules/outside_in/src/Render/MainContent/AjaxRenderer.php b/core/modules/outside_in/src/Render/MainContent/AjaxRenderer.php new file mode 100644 index 0000000..596824b --- /dev/null +++ b/core/modules/outside_in/src/Render/MainContent/AjaxRenderer.php @@ -0,0 +1,93 @@ +elementInfoManager->getInfo('ajax'); + $error = $main_content['#error']; + if (!empty($error)) { + // Fall back to some default message otherwise use the specific one. + if (!is_string($error)) { + $error = 'An error occurred while handling the request: The server received invalid input.'; + } + $response->addCommand(new AlertCommand($error)); + } + } + + $html = $this->drupalRenderRoot($main_content); + $response->setAttachments($main_content['#attached']); + + // The selector for the insert command is NULL as the new content will + // replace the element making the Ajax call. The default 'replaceWith' + // behavior can be changed with #ajax['method']. + $response->addCommand(new InsertCommand(NULL, $html)); + + if ($this->hasErrors()) { + $status_messages = ['#type' => 'status_messages']; + $output = $this->drupalRenderRoot($status_messages); + // If there are any status messages replace the existing messages. + $options = $request->request->get('formOptions', []); + + $messages_selector = isset($options['messagesSelector']) ? $options['messagesSelector'] : NULL; + $response->addCommand(new ReplaceCommand($messages_selector, $output)); + } + else { + if ($destination = $request->query->get('destination')) { + // If an editable id is specified add to query string to open dialog. + if ($editable_id = $request->query->get('editable_id')) { + $destination .= "?editable_id=" . $editable_id; + } + $url = Url::fromUserInput('/' . $destination); + + $response->addCommand(new RedirectCommand($url->setAbsolute()->toString())); + } + } + return $response; + } + + /** + * Determine if there are errors on the page. + * + * @todo This is a hacky way to figure if there are form errors. + * + * @return bool + * TRUE if there were any errors, FALSE otherwise. + */ + protected function hasErrors() { + $messages = drupal_get_messages(NULL, FALSE); + foreach ($messages as $key => $list) { + if ($key !== 'status' && $list) { + return TRUE; + } + } + return FALSE; + } + +} diff --git a/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php b/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php index 5d24c09..acc4012 100644 --- a/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php +++ b/core/modules/outside_in/src/Render/MainContent/OffCanvasRender.php @@ -41,6 +41,15 @@ public function __construct(TitleResolverInterface $title_resolver, RendererInte public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) { $response = new AjaxResponse(); + // Add place holder form messages from Ajax form requests. + $main_content = [ + 'off_canvas_messages' => [ + '#type' => 'container', + '#attributes' => ['class' => ['messages__wrapper']], + '#weight' => 100, + ], + ] + $main_content; + // First render the main content, because it might provide a title. $content = $this->renderer->renderRoot($main_content); 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 index 761693b..96319dc 100644 --- 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 @@ -27,3 +27,10 @@ offcanvas_test.dialog_links: _controller: '\Drupal\offcanvas_test\Controller\TestController::otherDialogLinks' requirements: _access: 'TRUE' + +offcanvas_test.form: + path: '/offcanvas-form' + defaults: + _form: '\Drupal\offcanvas_test\Form\TestForm' + 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 index 9ee8e17..518233b 100644 --- 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 @@ -92,6 +92,21 @@ public function linksDisplay() { ], ], ], + 'offcanvas_form' => [ + '#title' => 'Show form!', + '#type' => 'link', + '#url' => Url::fromRoute('offcanvas_test.form'), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'offcanvas', + ], + '#attached' => [ + 'library' => [ + 'outside_in/drupal.outside_in', + ], + ], + ], ]; } diff --git a/core/modules/outside_in/tests/modules/offcanvas_test/src/Form/TestForm.php b/core/modules/outside_in/tests/modules/offcanvas_test/src/Form/TestForm.php new file mode 100644 index 0000000..8f8d6a0 --- /dev/null +++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Form/TestForm.php @@ -0,0 +1,60 @@ + 'submit', + '#value' => 'Valid', + '#attributes' => ['class' => ['use-ajax-submit']], + ]; + $form['invalid'] = [ + '#type' => 'submit', + '#value' => 'Invalid', + '#attributes' => ['class' => ['use-ajax-submit']], + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + if ($form_state->getValue('op') == 'Invalid') { + $form_state->setErrorByName('invalid', 'Validation error'); + } + } + + /** + * Form submission handler. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + drupal_set_message('submitted'); + } + +} diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php index b81c723..b2cb676 100644 --- a/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php +++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php @@ -103,4 +103,19 @@ public function testNarrowWidth() { } } + /** + * Test form errors in the Off-Canvas dialog. + */ + public function testFormErrors() { + $web_assert = $this->assertSession(); + $this->drupalGet('/offcanvas-test-links'); + $page = $this->getSession()->getPage(); + $page->clickLink('Show form!'); + $this->waitForOffCanvasToOpen(); + $web_assert->elementExists('css', '.messages__wrapper'); + $page->pressButton('Invalid'); + $web_assert->assertWaitOnAjaxRequest(); + $web_assert->elementContains('css', '#drupal-offcanvas', 'Validation error'); + } + }