diff --git a/core/composer.json b/core/composer.json index 550c781..1c7fc8a 100644 --- a/core/composer.json +++ b/core/composer.json @@ -109,6 +109,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/offcanvas.css b/core/modules/outside_in/css/offcanvas.css new file mode 100644 index 0000000..c4d387c --- /dev/null +++ b/core/modules/outside_in/css/offcanvas.css @@ -0,0 +1,43 @@ +#offcanvas { + background: #3c3c3c; + color: #fff; + + box-sizing: border-box; + z-index: 1000; + + position: fixed; + height: 100%; + overflow-y: auto; + + border-left: 1px solid #000; + box-shadow: -2px 3px 1px 1px rgba(0, 0, 0, 0.3333); + + display: inline-block; + + width: 25%; +} + +#offcanvas span.offcanvasClose { + float: right; + margin: 15px 15px 0 0; +} + +#offcanvas h1 { + margin: 0 0 15px 0; + padding: 15px; + color: #fff; + border-bottom: 1px solid #fff; +} + +#offcanvas .content { + padding: 15px; +} + +#canvas-tray.offCanvasDisplayInProgress { + position: fixed; + display: inline-block; +} + +#canvas-tray.offCanvasDisplayed { + display: inline-block; +} diff --git a/core/modules/outside_in/css/outside_in.base.css b/core/modules/outside_in/css/outside_in.base.css new file mode 100644 index 0000000..ddf3b38 --- /dev/null +++ b/core/modules/outside_in/css/outside_in.base.css @@ -0,0 +1,184 @@ +/** + * @file + * Resets for Outside-In module. + */ + +/** + * Resets + * inside the wrapper override font styling the page provides, this may look a bit extreme + * but any module may place any new form element in this tray therefore CSS from any + * front-end-theme needs to be overridden + */ +#offcanvas abbr, +#offcanvas acronym, +#offcanvas address, +#offcanvas article, +#offcanvas aside, +#offcanvas audio, +#offcanvas b, +#offcanvas caption, +#offcanvas center, +#offcanvas cite, +#offcanvas code, +#offcanvas dd, +#offcanvas del, +#offcanvas details, +#offcanvas dfn, +#offcanvas dialog, +#offcanvas div, +#offcanvas dl, +#offcanvas dt, +#offcanvas em, +#offcanvas embed, +#offcanvas fieldset, +#offcanvas figcaption, +#offcanvas figure, +#offcanvas font, +#offcanvas footer, +#offcanvas form, +#offcanvas h1, +#offcanvas h2, +#offcanvas h3, +#offcanvas h4, +#offcanvas h5, +#offcanvas h6, +#offcanvas header, +#offcanvas hgroup, +#offcanvas i, +#offcanvas img, +#offcanvas ins, +#offcanvas kbd, +#offcanvas label, +#offcanvas legend, +#offcanvas li, +#offcanvas mark, +#offcanvas menu, +#offcanvas meter, +#offcanvas nav, +#offcanvas object, +#offcanvas ol, +#offcanvas output, +#offcanvas p, +#offcanvas pre, +#offcanvas progress, +#offcanvas q, +#offcanvas rp, +#offcanvas rt, +#offcanvas ruby, +#offcanvas s, +#offcanvas samp, +#offcanvas section, +#offcanvas small, +#offcanvas span, +#offcanvas strike, +#offcanvas strong, +#offcanvas sub, +#offcanvas summary, +#offcanvas sup, +#offcanvas table, +#offcanvas tbody, +#offcanvas td, +#offcanvas tfoot, +#offcanvas th, +#offcanvas thead, +#offcanvas time, +#offcanvas tr, +#offcanvas tt, +#offcanvas u, +#offcanvas ul, +#offcanvas var, +#offcanvas video { + font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif; + color: #eee; + font-size: 13px; + font-weight: normal; + font-style: normal; + line-height: 1.5em; + text-transform: none; + text-align: left; /* LTR */ + text-indent: 0; + padding:0; +} +/* reset font family rules from formalize to match Druapl (seven theme) */ +#offcanvas ::-webkit-validation-bubble-message, +#offcanvas optgroup, +#offcanvas .ie6_input, +#offcanvas * html textarea, +#offcanvas * html select, +#offcanvas textarea, +#offcanvas select, +#offcanvas button, +#offcanvas a.button, +#offcanvas input[type="date"], +#offcanvas input[type="datetime"], +#offcanvas input[type="datetime-local"], +#offcanvas input[type="email"], +#offcanvas input[type="month"], +#offcanvas input[type="number"], +#offcanvas input[type="password"], +#offcanvas input[type="search"], +#offcanvas input[type="tel"], +#offcanvas input[type="text"], +#offcanvas input[type="time"], +#offcanvas input[type="url"], +#offcanvas input[type="week"], +#offcanvas input[type="reset"], +#offcanvas input[type="submit"], +#offcanvas input[type="button"] { + font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif; +} +/* reset the link color to one that stands out on dark background */ +#offcanvas a { + color: #33aaff; + text-decoration: none; +} +/* reset to sensible vertical rhythm */ +#offcanvas .form-item, +#offcanvas details, +#offcanvas button, +.form-type-checkbox, +.form-type-radio { + margin-top: 0; + margin-bottom: 1.25em; + padding-right: .5em; /* LTR */ + display: inline-block; +} +/* re-set defaults for fieldsets */ +#offcanvas .fieldset-wrapper { + margin: 0; + padding: 0; + border: 0; +} +/* override formalize label styling */ +#offcanvas label { + font-size: 85%; + font-weight: bold; + line-height: 1.5em; +} +/* override formalize button styling */ +#offcanvas button, +#offcanvas .button, +#offcanvas input[type="button"], +#offcanvas input[type="reset"] { + font-weight: normal; + text-shadow: none; + width: auto; + max-width: 50%; + color: #fff; + border-radius: 30px; + border: none; + background-color: #777; + background-image: none; + background-image: none; +} + /* remove browser and formalize focus effects for dark background */ +#offcanvas input:focus, +#offcanvas button:focus, +#offcanvas a.button:focus, +#offcanvas select:focus, +#offcanvas textarea:focus { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + outline: none; +} 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..fa659a7 --- /dev/null +++ b/core/modules/outside_in/css/outside_in.module.css @@ -0,0 +1,213 @@ +/** + * @file + * Styling for Outside-In module. + */ + +/* style the offcanvas container */ +#offcanvas { + box-sizing: border-box; + position: fixed; + height: 100%; + overflow-y: auto; + box-shadow: -2px 3px 1px 1px rgba(0, 0, 0, 0.3333); /* LTR */ + display: inline-block; + width: 25%; + -webkit-transform: translate(100%, 0); + -moz-transform: translate(100%, 0); + -o-transform: translate(100%, 0); + -ms-transform: translate(100%, 0); + transform: translate(100%, 0); + transition: all 0.75s; + -webkit-transition: all 0.75s; + -moz-transition: all 0.75s; + z-index: 1000; +} +[dir="rtl"] #offcanvas { + right: auto; + left: 0; +} +#canvas-tray-wrapper #canvas-tray { + width: 100%; + display: inline-block; + transition: all 0.75s; + -webkit-transition: all 0.75s; + -moz-transition: all 0.75s; +} +#canvas-tray-wrapper.js-tray-open #canvas-tray { + width: 75%; +} +#canvas-tray-wrapper.js-tray-open #offcanvas { + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} +/* button that closes the offcanvas tray */ +#offcanvas > button.offcanvasClose, +#offcanvas > button.offcanvasClose:hover { /* todo: add hover state */ + position: absolute; + width: 30px; + right: 0; /* LTR */ + top: 1em; + border: 0; + border-radius: 0; + background: url(/core/misc/icons/ffffff/ex.svg) 0 0 no-repeat; + color: transparent; + cursor: pointer; +} +[dir="rtl"] #offcanvas > span.offcanvasClose { + right: auto; + left: 0; +} +#offcanvas > .content { + height: 100%; + padding: 15px; +} +#canvas-tray.offCanvasDisplayInProgress { + position: fixed; + display: inline-block; +} +#canvas-tray.offCanvasDisplayed { + display: inline-block; +} + +/** + * Buttons. + */ + +/* style for primary button */ +#offcanvas .button--primary { + width: auto; + max-width: 50%; + border-radius: 30px; + border: none; + background-image: -webkit-linear-gradient(top,#007bc6,#0071b8); + background-image: linear-gradient(to bottom,#007bc6,#0071b8); +} +#offcanvas .button--primary:hover, +#offcanvas .button--primary:active { + background-color: inherit; +} +/* Style for delete button */ +#offcanvas .button--danger { + width: auto; + max-width: 50%; + border-radius: none; + border: none; + background: transparent; +} +/* special edit link to flow inline with form items that link to more configuration */ +#offcanvas .edit-link { /* todo add markup and class to template */ + display: inline-block; + font-size: 85%; + font-weight: bold; +} + +/** + * Inputs. + */ +#offcanvas textarea, +#offcanvas select, +#offcanvas input[type="date"], +#offcanvas input[type="datetime"], +#offcanvas input[type="datetime-local"], +#offcanvas input[type="email"], +#offcanvas input[type="month"], +#offcanvas input[type="number"], +#offcanvas input[type="password"], +#offcanvas input[type="search"], +#offcanvas input[type="tel"], +#offcanvas input[type="text"], +#offcanvas input[type="time"], +#offcanvas input[type="url"], +#offcanvas input[type="week"], +#offcanvas input[type="range"] { + display: block; + width: 100%; +} +/* reduce the size of descriptions and other elements for scanability */ +#offcanvas .description, +#offcanvas .machine-name-label, +#offcanvas .machine-name-value { + font-size: 85%; + font-weight: normal; + padding-top: .25em; +} + +/* add a background to checkboxes, radois for contrast */ +/* +#offcanvas .radio-wrapper { + display: inline-block; + padding: 1px; + border-radius: 20px; +} +#offcanvas .checkbox-wrapper { + display: inline-block; + padding: 1px; + border-radius: 3px; +} +*/ + +/** + * Fieldsets. + */ +#offcanvas fieldset { + border: 0px solid transparent; + margin-left:-1em; /* push background to the left edge of the parent */ + margin-right:-1em; /* push background to the right edge of the parent */ + padding-top: 2.5em; + padding-left: 1em; + padding-bottom: 1em; + position: relative; +} +#offcanvas fieldset .form-item { + display: block; +} +/* style fieldset legend similar to Drupal (seven theme) */ +#offcanvas span.fieldset-legend { + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 85%; + position: absolute; + top: 10px; +} + +/** + * Admin label. + */ +#offcanvas .form-item-settings-admin-label { + font-size: 1.25em; + width: 100%; + padding: 1em; + padding-top: 0; + margin-top: 0; + margin-left: -1em; + margin-right: -1em; + margin-bottom: 1em; + border-bottom-width: 1px; + border-bottom-style: solid; +} +#offcanvas .form-item-settings-admin-label label { + font-size: 10px; + display: block; + margin-bottom: 0; +} + +#offcanvas span.field-suffix small { /* don't know why it's there. todo: figure out why it's rendering empty and taking space */ + display: none; +} + +/** + * Form actions. + */ +#offcanvas .form-actions { + padding: 1em; + margin-left: -1em; + margin-right: -1em; + border-bottom-width: 1px; + border-bottom-style: solid; +} + + + 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..acd176d --- /dev/null +++ b/core/modules/outside_in/css/outside_in.theme.css @@ -0,0 +1,128 @@ +/** + * @file + * Visual styling for Outside-In module. + * + * Since visual styling here differs from styling elsewhere in core, + * font and color styles are put here so they can be more easily changed. + * + */ + +/* style the offcanvas container */ +#offcanvas { + color: #eee; + border-left: 1px solid #000; + background: #444; + font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif; +} +#offcanvas .content { + color: #eee; +} +#offcanvas h1 { + margin: 0 0 15px 0; + padding: 15px; + color: #eee; + border-bottom: 1px solid #777; +} + +/** + * Buttons. + */ +#offcanvas button:hover, +#offcanvas button:active, +#offcanvas .button:hover, +#offcanvas .button:active { + background-color: inherit; + background-image: inherit; + background-image: inherit; +} +/* style for primary button */ +#offcanvas .button--primary { + background-image: -webkit-linear-gradient(top,#007bc6,#0071b8); + background-image: linear-gradient(to bottom,#007bc6,#0071b8); + font-weight: bold; + text-shadow: none; + color: #fff; +} +#offcanvas .button--primary:hover, +#offcanvas .button--primary:active { + background-color: inherit; + background-image: -webkit-linear-gradient(top,#007bc6,#0071b8); /* todo: make the hover gradient different from normal */ + background-image: linear-gradient(to bottom,#007bc6,#0071b8); /* todo: make the hover gradient different from normal */ +} +/* Style for delete button */ +#offcanvas .button--danger { + font-weight: bold; + text-shadow: none; + color: #ff3333; +} + +/** + * Inputs. + */ +#offcanvas textarea, +#offcanvas select, +#offcanvas input[type="date"], +#offcanvas input[type="datetime"], +#offcanvas input[type="datetime-local"], +#offcanvas input[type="email"], +#offcanvas input[type="month"], +#offcanvas input[type="number"], +#offcanvas input[type="password"], +#offcanvas input[type="search"], +#offcanvas input[type="tel"], +#offcanvas input[type="text"], +#offcanvas input[type="time"], +#offcanvas input[type="url"], +#offcanvas input[type="week"], +#offcanvas input[type="range"] { + background-color: #ddd; /* darken inputs to reduce contrast on dark background */ + border: 1px solid #333; +} +/* add focus effect to fields for dark background */ +#offcanvas textarea:focus, +#offcanvas select:focus, +#offcanvas input:focus { + background-color: #fff; +} +/* reduce the size of descriptions and other elements for scanability */ +#offcanvas .description, +#offcanvas .machine-name-label, +#offcanvas .machine-name-value { + font-family: 'Lucida Grande','Lucida Sans Unicode','Liberation Sans',sans-serif; + color: #bbb; +} +/* add a background to radio buttons for contrast */ +/* +#offcanvas .radio-wrapper { + background-color: #000; +} +#offcanvas .checkbox-wrapper { + background-color: #000; +} +*/ + +/** + * Fieldsets. + */ +#offcanvas fieldset { + background-color: #333; +} + +/** + * Admin label. + */ +#offcanvas .form-item-settings-admin-label { + border-bottom-color: #777; +} +#offcanvas .form-item-settings-admin-label label { + color: #bbb; +} + +/** + * Form actions. + */ +#offcanvas .form-actions { + border-bottom-color: #777; +} + + diff --git a/core/modules/outside_in/js/offcanvas.js b/core/modules/outside_in/js/offcanvas.js new file mode 100644 index 0000000..3f4a958 --- /dev/null +++ b/core/modules/outside_in/js/offcanvas.js @@ -0,0 +1,122 @@ +/** + * @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: '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 {object} + * 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(). + * + * Opens tag consistant wrapping to html.html.twig for all themes. + */ +function outside_in_page_top(array &$page_top) { + // TODO: rename 'outside_in' longterm. + $page_top['outside_in'] = [ + '#markup' => '
', + '#weight' => 1000 + ]; +} + +/** + * Implements hook_page_bottom(). + * + * Closes tag consistant wrapping to html.html.twig for all themes. + */ +function outside_in_page_bottom(array &$page_top) { + // TODO: rename 'outside_in' longterm. + $page_top['outside_in'] = [ + '#markup' => '
', + '#weight' => -1000 + ]; +} + +/** + * Implements hook_block_alter(). + */ +function outside_in_block_alter(&$definitions) { + foreach ($definitions as &$definition) { + // If no default form is defined and this plugin implements + // \Drupal\Core\Plugin\PluginFormInterface, use that for the default form. + if (!isset($definition['form']['default']) && is_subclass_of($definition['class'], PluginFormInterface::class)) { + $definition['form']['default'] = $definition['class']; + } + } +} 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..d32143c --- /dev/null +++ b/core/modules/outside_in/outside_in.services.yml @@ -0,0 +1,9 @@ +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 } + outside_in.block.manager: + class: Drupal\outside_in\OutsideInBlockManager + arguments: ['@class_resolver'] 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..d61ed4e --- /dev/null +++ b/core/modules/outside_in/src/Ajax/OpenOffCanvasDialogCommand.php @@ -0,0 +1,54 @@ +dialogOptions['modal'] = FALSE; + return array( + 'command' => 'openOffCanvas', + 'selector' => $this->selector, + 'settings' => $this->settings, + 'data' => $this->getRenderedContent(), + 'dialogOptions' => $this->dialogOptions, + ); + } + +} diff --git a/core/modules/outside_in/src/OperationAwareFormInterface.php b/core/modules/outside_in/src/OperationAwareFormInterface.php new file mode 100644 index 0000000..cf31c58 --- /dev/null +++ b/core/modules/outside_in/src/OperationAwareFormInterface.php @@ -0,0 +1,20 @@ +classResolver = $class_resolver; + } + + /** + * {@inheritdoc} + */ + public function getFormObject(PluginInspectionInterface $plugin, $operation) { + $definition = $plugin->getPluginDefinition(); + if (!isset($definition['form'][$operation])) { + throw new InvalidPluginDefinitionException($plugin->getPluginId(), sprintf('The "%s" plugin did not specify a "%s" form class', $plugin->getPluginId(), $operation)); + } + + // If the form specified is the plugin itself, use it directly. + if (get_class($plugin) === $definition['form'][$operation]) { + $form_object = $plugin; + } + else { + $form_object = $this->classResolver->getInstanceFromDefinition($definition['form'][$operation]); + } + + // Ensure the resulting object is a plugin form. + if (!$form_object instanceof PluginFormInterface) { + throw new InvalidPluginDefinitionException($plugin->getPluginId(), sprintf('The "%s" plugin did not specify a valid "%s" form class, must implement \Drupal\Core\Plugin\PluginFormInterface', $plugin->getPluginId(), $operation)); + } + + if ($form_object instanceof OperationAwareFormInterface) { + $form_object->setOperation($operation); + } + + return $form_object; + } + +} diff --git a/core/modules/outside_in/src/OutsideInBlockManagerInterface.php b/core/modules/outside_in/src/OutsideInBlockManagerInterface.php new file mode 100644 index 0000000..17c9c15 --- /dev/null +++ b/core/modules/outside_in/src/OutsideInBlockManagerInterface.php @@ -0,0 +1,27 @@ +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 OpenModalDialogCommand 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', array()); + + $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..3ad22a8 --- /dev/null +++ b/core/modules/outside_in/src/Tests/Ajax/OffCanvasDialogTest.php @@ -0,0 +1,51 @@ +drupalLogin($this->drupalCreateUser(array('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 = array( + 'command' => 'openOffCanvas', + 'selector' => '#drupal-offcanvas', + 'settings' => NULL, + 'data' => $dialog_contents, + 'dialogOptions' => array( + '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', array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_offcanvas'))); + $this->assertEqual($offcanvas_expected_response, $ajax_result[3], 'Offcanvas 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..e21810e --- /dev/null +++ b/core/modules/outside_in/tests/modules/offcanvas_test/offcanvas_test.info.yml @@ -0,0 +1,9 @@ +name: 'Offcanvas tests' +type: module +description: 'Provides offcanvas 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..8bf28ad --- /dev/null +++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Controller/TestController.php @@ -0,0 +1,77 @@ + 'markup', + '#markup' => 'Thing 1 says hello', + ]; + } + + /** + * Thing2. + * + * @return string + * Return Hello string. + */ + public function thing2() { + return [ + '#type' => 'markup', + '#markup' => 'Thing 2 says hello', + ]; + } + + /** + * Display 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/Form/OffcanvasForm.php b/core/modules/outside_in/tests/modules/offcanvas_test/src/Form/OffcanvasForm.php new file mode 100644 index 0000000..0e6c526 --- /dev/null +++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Form/OffcanvasForm.php @@ -0,0 +1,47 @@ +operation = $operation; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + // Intentionally empty. + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + // Intentionally empty. + } + +} 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..629a902 --- /dev/null +++ b/core/modules/outside_in/tests/modules/offcanvas_test/src/Plugin/Block/TestBlock.php @@ -0,0 +1,52 @@ + [ + '#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..52ebaaf --- /dev/null +++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php @@ -0,0 +1,83 @@ +install([$theme]); + $theme_config = \Drupal::configFactory()->getEditable('system.theme'); + $theme_config->set('default', $theme); + $theme_config->save(); + //$this->placeBlock('offcanvas_links_block', ['id' => 'offcanvaslinks']); + $this->drupalGet('/offcanvas-test-links'); + + $page = $this->getSession()->getPage(); + + $this->htmlOutput($page->getContent()); + $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!"); + $web_assert->assertWaitOnAjaxRequest(); + // Wait for off-canvas element to appear. + $condition = "(jQuery('#offcanvas').length > 0)"; + $this->assertJsCondition($condition, 5000); + + // 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 = $page->findById('offcanvas'); + + // 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', '.content')->getText(); + $this->assertEquals("Thing $link_index says hello", $tray_text); + + // Close the tray. + // @todo Should the close have an id? + $offcanvas_tray->find('css', '.offcanvasClose')->press(); + // Wait for animation to be done. + $web_assert->assertWaitOnAjaxRequest(); + // Make sure canvas doesn't exist after closing. + $web_assert->elementNotExists('css', '#offcanvas'); + } + + $this->verbose('Test theme: ' . $theme); + } + } + +} diff --git a/core/modules/outside_in/tests/src/Kernel/MultipleBlockFormTest.php b/core/modules/outside_in/tests/src/Kernel/MultipleBlockFormTest.php new file mode 100644 index 0000000..302a145 --- /dev/null +++ b/core/modules/outside_in/tests/src/Kernel/MultipleBlockFormTest.php @@ -0,0 +1,37 @@ +createInstance('offcanvas_links_block'); + + $form_object1 = \Drupal::service('outside_in.block.manager')->getFormObject($block, 'default'); + $form_object2 = \Drupal::service('outside_in.block.manager')->getFormObject($block, 'sidebar'); + + // Assert that the block itself is used for the default form. + $this->assertSame($block, $form_object1); + + $expected_sidebar = new SidebarForm(); + $expected_sidebar->setOperation('sidebar'); + $this->assertEquals($expected_sidebar, $form_object2); + } + +} diff --git a/core/modules/outside_in/tests/src/Unit/Ajax/AjaxCommandsTest.php b/core/modules/outside_in/tests/src/Unit/Ajax/AjaxCommandsTest.php new file mode 100644 index 0000000..d907b6c --- /dev/null +++ b/core/modules/outside_in/tests/src/Unit/Ajax/AjaxCommandsTest.php @@ -0,0 +1,47 @@ +getMockBuilder('Drupal\outside_in\Ajax\OpenOffCanvasDialogCommand') + ->setConstructorArgs(array( + 'Title', '

Text!

', array( + 'url' => 'example', + ), + )) + ->setMethods(array('getRenderedContent')) + ->getMock(); + + // This method calls the render service, which isn't available. We want it + // to do nothing so we mock it to return a known value. + $command->expects($this->once()) + ->method('getRenderedContent') + ->willReturn('rendered content'); + + $expected = array( + 'command' => 'openOffCanvas', + 'selector' => '#drupal-offcanvas', + 'settings' => NULL, + 'data' => 'rendered content', + 'dialogOptions' => array( + 'url' => 'example', + 'title' => 'Title', + 'modal' => FALSE, + ), + ); + $this->assertEquals($expected, $command->render()); + } + +} diff --git a/core/modules/outside_in/tests/src/Unit/OutsideInBlockManagerTest.php b/core/modules/outside_in/tests/src/Unit/OutsideInBlockManagerTest.php new file mode 100644 index 0000000..a3d7b58 --- /dev/null +++ b/core/modules/outside_in/tests/src/Unit/OutsideInBlockManagerTest.php @@ -0,0 +1,138 @@ +classResolver = $this->prophesize(ClassResolverInterface::class); + $this->manager = new OutsideInBlockManager($this->classResolver->reveal()); + } + + /** + * @covers ::getFormObject + */ + public function testGetFormObject() { + $plugin_form = $this->prophesize(PluginFormInterface::class); + $expected = $plugin_form->reveal(); + + $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected); + + $plugin = $this->prophesize(PluginInspectionInterface::class); + $plugin->getPluginDefinition()->willReturn([ + 'form' => [ + 'standard_class' => get_class($expected), + ], + ]); + + $form_object = $this->manager->getFormObject($plugin->reveal(), 'standard_class'); + $this->assertSame($expected, $form_object); + } + + /** + * @covers ::getFormObject + */ + public function testGetFormObjectUsingPlugin() { + $this->classResolver->getInstanceFromDefinition(Argument::cetera())->shouldNotBeCalled(); + + $plugin = $this->prophesize(PluginInspectionInterface::class)->willImplement(PluginFormInterface::class); + $plugin->getPluginDefinition()->willReturn([ + 'form' => [ + 'default' => get_class($plugin->reveal()), + ], + ]); + + $form_object = $this->manager->getFormObject($plugin->reveal(), 'default'); + $this->assertSame($plugin->reveal(), $form_object); + } + + /** + * @covers ::getFormObject + */ + public function testGetFormObjectOperationAware() { + $plugin_form = $this->prophesize(PluginFormInterface::class)->willImplement(OperationAwareFormInterface::class); + $plugin_form->setOperation('operation_aware')->shouldBeCalled(); + + $expected = $plugin_form->reveal(); + + $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected); + + $plugin = $this->prophesize(PluginInspectionInterface::class); + $plugin->getPluginDefinition()->willReturn([ + 'form' => [ + 'operation_aware' => get_class($expected), + ], + ]); + + $form_object = $this->manager->getFormObject($plugin->reveal(), 'operation_aware'); + $this->assertSame($expected, $form_object); + } + + /** + * @covers ::getFormObject + */ + public function testGetFormObjectDefinitionException() { + $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "the_plugin_id" plugin did not specify a "anything" form class'); + + $plugin = $this->prophesize(PluginInspectionInterface::class); + $plugin->getPluginId()->willReturn('the_plugin_id'); + $plugin->getPluginDefinition()->willReturn([]); + + $form_object = $this->manager->getFormObject($plugin->reveal(), 'anything'); + $this->assertSame(NULL, $form_object); + } + + /** + * @covers ::getFormObject + */ + public function testGetFormObjectInvalidException() { + $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "the_plugin_id" plugin did not specify a valid "invalid" form class, must implement \Drupal\Core\Plugin\PluginFormInterface'); + + $expected = new \stdClass(); + $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected); + + $plugin = $this->prophesize(PluginInspectionInterface::class); + $plugin->getPluginId()->willReturn('the_plugin_id'); + $plugin->getPluginDefinition()->willReturn([ + 'form' => [ + 'invalid' => get_class($expected), + ], + ]); + + $form_object = $this->manager->getFormObject($plugin->reveal(), 'invalid'); + $this->assertSame(NULL, $form_object); + } + +}