diff --git a/config/schema/entity_browser.schema.yml b/config/schema/entity_browser.schema.yml index 2f09d9f..7416799 100644 --- a/config/schema/entity_browser.schema.yml +++ b/config/schema/entity_browser.schema.yml @@ -108,6 +108,9 @@ entity_browser.browser.widget.view: submit_text: type: string label: 'Submit button text' + auto_select: + type: boolean + label: 'Automatically submit selection' view: type: string label: 'View ID' diff --git a/entity_browser.libraries.yml b/entity_browser.libraries.yml index 3141148..8448650 100644 --- a/entity_browser.libraries.yml +++ b/entity_browser.libraries.yml @@ -89,6 +89,7 @@ view: multi_step_display: version: VERSION js: + js/entity_browser.command_queue.js: {} js/entity_browser.multi_step_display.js: {} dependencies: - entity_browser/entity_list diff --git a/js/entity_browser.command_queue.js b/js/entity_browser.command_queue.js new file mode 100644 index 0000000..48c4287 --- /dev/null +++ b/js/entity_browser.command_queue.js @@ -0,0 +1,108 @@ +/** + * @file entity_browser.command_queue.js + */ + +(function ($, Drupal) { + + 'use strict'; + + /** + * Namespace for command queue functionality. + * + * Command queue provides functionality to queue ajax commands in front-end + * and execute them in one ajax request in backend. All commands triggered + * during ajax request execution, will be queue and executed after response + * from previous ajax request is received. + * + * @type {Object} + */ + Drupal.entityBrowserCommandQueue = {}; + + /** + * Queue container for keeping commands for next queue execution. (protected) + * + * @type {Object} + */ + var commandsQueue = {}; + + /** + * Registers behaviours related to Ajax commands execution. + */ + Drupal.behaviors.entityBrowserCommandQueue = { + attach: function (context) { + var handler = $(context).find('[name="ajax_commands_handler"]'); + + handler.once('register-execute-commands') + .bind('execute-commands', Drupal.entityBrowserCommandQueue.executeCommands); + } + }; + + /** + * Action to queue command for future execution. + * + * @param {string} commandName + * Command name, that will be executed. + * @param {Array} commandParam + * Params for command. If command already exists in queue params will be + * added to end of list. + */ + Drupal.entityBrowserCommandQueue.queueCommand = function (commandName, commandParam) { + if (!commandsQueue[commandName]) { + commandsQueue[commandName] = []; + } + + commandsQueue[commandName].push(commandParam); + }; + + /** + * Handler for executing queued commands over Ajax. + * + * @param {object} event + * Event object. + * @param {boolean} addedCommand + * Execution of queued commands is triggered after new command is added. + */ + Drupal.entityBrowserCommandQueue.executeCommands = function (event, addedCommand) { + var handler = $(this); + var handlerElement = handler[0]; + var runningAjax = Drupal.entityBrowserCommandQueue.isAjaxRunning(handlerElement, 'execute_js_commands'); + var filledQueue = !$.isEmptyObject(commandsQueue); + + if (!runningAjax && filledQueue) { + handler.val(JSON.stringify(commandsQueue)); + + // Clear Queue after command is set to handler element. + commandsQueue = {}; + + // Trigger event to execute event with defined command. + handler.trigger('execute_js_commands'); + } + else if (!addedCommand && filledQueue) { + setTimeout($.proxy(Drupal.entityBrowserCommandQueue.executeCommands, handlerElement), 200); + } + }; + + /** + * Search is there current Ajax request executing for current event. + * + * @param {element} handlerElement + * Element on what event is triggered. + * @param {string} eventName + * Event name. + * + * @return {boolean} + * Returns true if ajax event is still running for element. + */ + Drupal.entityBrowserCommandQueue.isAjaxRunning = function (handlerElement, eventName) { + var ajax_list = Drupal.ajax.instances; + + for (var i = 0; i < ajax_list.length; i++) { + if (ajax_list[i] && ajax_list[i].event === eventName && ajax_list[i].element === handlerElement && ajax_list[i].ajaxing) { + return true; + } + } + + return false; + }; + +}(jQuery, Drupal)); diff --git a/js/entity_browser.multi_step_display.js b/js/entity_browser.multi_step_display.js index 2360f02..9fe1d9e 100644 --- a/js/entity_browser.multi_step_display.js +++ b/js/entity_browser.multi_step_display.js @@ -16,6 +16,24 @@ stop: Drupal.entityBrowserMultiStepDisplay.entitiesReordered }); + // Register add/remove entities event handlers. + $entities.once('register-add-entities') + .bind('add-entities', Drupal.entityBrowserMultiStepDisplay.addEntities); + + $entities.once('register-remove-entities') + .bind('remove-entities', Drupal.entityBrowserMultiStepDisplay.removeEntities); + + // Register event for remove button to use AJAX event. + var $remove_buttons = $entities.find('.entity-browser-remove-selected-entity'); + $remove_buttons.once('register-click').on('click', function (event) { + event.preventDefault(); + + var button_element = $(event.target); + var remove_entity_id = button_element.attr('data-remove-entity') + '_' + button_element.attr('data-row-id'); + + $entities.trigger('remove-entities', [[remove_entity_id]]); + }); + // Add a toggle button for the display of selected entities. var $toggle = $('.entity-browser-show-selection'); @@ -27,12 +45,14 @@ } } - $toggle.on('click', function (e) { - e.preventDefault(); - $entities.slideToggle('fast', setToggleText); - }); + if ($entities.length > 0) { + $toggle.once('register-click').on('click', function (e) { + e.preventDefault(); + $entities.slideToggle('fast', setToggleText); + }); - setToggleText(); + setToggleText(); + } } }; @@ -53,4 +73,67 @@ } }; + /** + * Remove entities from selection of multistep display. + * + * @param {object} event + * Event object. + * @param {Array} entity_ids + * Entity IDs that should be removed from selection. + */ + Drupal.entityBrowserMultiStepDisplay.removeEntities = function (event, entity_ids) { + var entities_list = $(this); + var i; + + for (i = 0; i < entity_ids.length; i++) { + // Remove dom element, and queue entity for removal in backend. + var element_selector = '[data-drupal-selector="edit-selected-'.concat(entity_ids[i].replace(/_/g, '-'), '"]'); + entities_list.find(element_selector).remove(); + + Drupal.entityBrowserCommandQueue.queueCommand( + 'remove', + { + entity_id: entity_ids[i] + } + ); + + // Remove action buttons, if there are no more entities selected. + if (entities_list.children().length === 0) { + entities_list.siblings('.entity-browser-use-selected').remove(); + entities_list.siblings('.entity-browser-show-selection').remove(); + } + } + + entities_list.siblings('[name=ajax_commands_handler]').trigger('execute-commands', [true]); + }; + + /** + * Add entities into selection of multistep display. + * + * @param {object} event + * Event object. + * @param {Array} entity_ids + * Entity ID that should be added to selection. + */ + Drupal.entityBrowserMultiStepDisplay.addEntities = function (event, entity_ids) { + var entities_list = $(this); + var i; + + for (i = 0; i < entity_ids.length; i++) { + // Add proxy element that will be replaced with returned Ajax Command. + var proxy_element = $('
').uniqueId(); + entities_list.append(proxy_element); + + Drupal.entityBrowserCommandQueue.queueCommand( + 'add', + { + entity_id: entity_ids[i], + proxy_id: proxy_element.attr('id') + } + ); + } + + entities_list.siblings('[name=ajax_commands_handler]').trigger('execute-commands', [true]); + }; + }(jQuery, Drupal)); diff --git a/js/entity_browser.view.js b/js/entity_browser.view.js index a4f33ad..921bebd 100644 --- a/js/entity_browser.view.js +++ b/js/entity_browser.view.js @@ -47,6 +47,36 @@ } }); }); + + // If "auto_select" functionality is enabled, then selection column is + // hidden and click on row will actually add element into selection + // display over javascript event. Currently only multistep display + // supports that functionality. + if (drupalSettings.entity_browser_widget.auto_select) { + var selection_cells = views_instance.$view.find('.views-field-entity-browser-select'); + + // Register on cell parents (rows) click event. + selection_cells.parent() + .once('register-row-click') + .click(function (event) { + event.preventDefault(); + + var $row = $(this); + + // Ensure to use input (checkbox) field from entity browser + // column dedicated for selection checkbox. + var $input = $row.find('.views-field-entity-browser-select input.form-checkbox'); + + // Get selection display element and trigger adding of entity + // over ajax request. + $row.parents('form') + .find('.entities-list') + .trigger('add-entities', [[$input.val()]]); + }); + + // Hide selection cells (selection column) with checkboxes. + selection_cells.hide(); + } } } }; diff --git a/modules/entity_form/src/Plugin/EntityBrowser/Widget/EntityForm.php b/modules/entity_form/src/Plugin/EntityBrowser/Widget/EntityForm.php index 05676cf..0ce8834 100644 --- a/modules/entity_form/src/Plugin/EntityBrowser/Widget/EntityForm.php +++ b/modules/entity_form/src/Plugin/EntityBrowser/Widget/EntityForm.php @@ -21,7 +21,8 @@ use Drupal\Core\Entity\EntityDisplayRepositoryInterface; * @EntityBrowserWidget( * id = "entity_form", * label = @Translation("Entity form"), - * description = @Translation("Provides entity form widget.") + * description = @Translation("Provides entity form widget."), + * auto_select = FALSE * ) */ class EntityForm extends WidgetBase { diff --git a/modules/example/config/install/core.entity_form_display.node.entity_browser_test.default.yml b/modules/example/config/install/core.entity_form_display.node.entity_browser_test.default.yml index 9dc617d..de7d83c 100644 --- a/modules/example/config/install/core.entity_form_display.node.entity_browser_test.default.yml +++ b/modules/example/config/install/core.entity_form_display.node.entity_browser_test.default.yml @@ -5,6 +5,7 @@ dependencies: - field.field.node.entity_browser_test.body - field.field.node.entity_browser_test.field_files - field.field.node.entity_browser_test.field_files1 + - field.field.node.entity_browser_test.field_files_over_ajax - field.field.node.entity_browser_test.field_image_browser - field.field.node.entity_browser_test.field_nodes - node.type.entity_browser_test @@ -55,6 +56,18 @@ content: field_widget_edit: true field_widget_remove: true third_party_settings: { } + field_files_over_ajax: + weight: 36 + settings: + entity_browser: test_files_ajax + field_widget_display: label + field_widget_edit: true + field_widget_remove: true + selection_mode: selection_edit + open: false + field_widget_display_settings: { } + third_party_settings: { } + type: entity_browser_entity_reference field_image_browser: weight: 35 settings: diff --git a/modules/example/config/install/core.entity_view_display.node.entity_browser_test.default.yml b/modules/example/config/install/core.entity_view_display.node.entity_browser_test.default.yml index 12bb96b..c4b8a3b 100644 --- a/modules/example/config/install/core.entity_view_display.node.entity_browser_test.default.yml +++ b/modules/example/config/install/core.entity_view_display.node.entity_browser_test.default.yml @@ -5,6 +5,7 @@ dependencies: - field.field.node.entity_browser_test.body - field.field.node.entity_browser_test.field_files - field.field.node.entity_browser_test.field_files1 + - field.field.node.entity_browser_test.field_files_over_ajax - field.field.node.entity_browser_test.field_image_browser - field.field.node.entity_browser_test.field_nodes - node.type.entity_browser_test @@ -40,6 +41,13 @@ content: link: true third_party_settings: { } type: entity_reference_label + field_files_over_ajax: + weight: 106 + label: above + settings: + link: true + third_party_settings: { } + type: entity_reference_label field_image_browser: weight: 105 label: above diff --git a/modules/example/config/install/entity_browser.browser.test_files_ajax.yml b/modules/example/config/install/entity_browser.browser.test_files_ajax.yml new file mode 100644 index 0000000..5b7e81a --- /dev/null +++ b/modules/example/config/install/entity_browser.browser.test_files_ajax.yml @@ -0,0 +1,44 @@ +langcode: und +status: true +dependencies: + enforced: + module: + - entity_browser_example + module: + - views +name: test_files_ajax +label: 'Test entity browser for files (with auto loading)' +display: iframe +display_configuration: + width: '650' + height: '500' + link_text: 'Select entities' + auto_open: false +selection_display: multi_step_display +selection_display_configuration: + entity_type: node + display: label + display_settings: { } + select_text: 'Use selected' + selection_hidden: false +widget_selector: tabs +widget_selector_configuration: { } +widgets: + a4ad947c-9669-497c-9988-24351955a02f: + settings: + view: files_entity_browser + view_display: entity_browser_1 + submit_text: 'Select entities' + auto_select: true + uuid: a4ad947c-9669-497c-9988-24351955a02f + weight: -10 + label: 'Files listing' + id: view + 735d146c-a4b2-4327-a057-d109e0905e05: + settings: + upload_location: 'public://' + submit_text: 'Select files' + uuid: 735d146c-a4b2-4327-a057-d109e0905e05 + weight: -9 + label: 'Upload files' + id: upload diff --git a/modules/example/config/install/field.field.node.entity_browser_test.field_files_over_ajax.yml b/modules/example/config/install/field.field.node.entity_browser_test.field_files_over_ajax.yml new file mode 100644 index 0000000..291ad83 --- /dev/null +++ b/modules/example/config/install/field.field.node.entity_browser_test.field_files_over_ajax.yml @@ -0,0 +1,24 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_files_over_ajax + - node.type.entity_browser_test +id: node.entity_browser_test.field_files_over_ajax +field_name: field_files_over_ajax +entity_type: node +bundle: entity_browser_test +label: 'Files (with auto loading)' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:file' + handler_settings: + target_bundles: null + sort: + field: _none + auto_create: false +field_type: entity_reference diff --git a/modules/example/config/install/field.storage.node.field_files_over_ajax.yml b/modules/example/config/install/field.storage.node.field_files_over_ajax.yml new file mode 100644 index 0000000..bb8a800 --- /dev/null +++ b/modules/example/config/install/field.storage.node.field_files_over_ajax.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - file + - node +id: node.field_files_over_ajax +field_name: field_files_over_ajax +entity_type: node +type: entity_reference +settings: + target_type: file +module: core +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/src/Annotation/EntityBrowserSelectionDisplay.php b/src/Annotation/EntityBrowserSelectionDisplay.php index f55babb..fe54f95 100644 --- a/src/Annotation/EntityBrowserSelectionDisplay.php +++ b/src/Annotation/EntityBrowserSelectionDisplay.php @@ -50,4 +50,16 @@ class EntityBrowserSelectionDisplay extends Plugin { */ public $acceptPreselection = FALSE; + /** + * Indicates that javascript commands can be executed for Selection display. + * + * Currently supported javascript commands are adding and removing selection + * from selection display. Javascript commands use Ajax requests to load + * relevant changes and makes user experience way better, becase form is not + * flashed every time. + * + * @var bool + */ + public $js_commands = FALSE; + } diff --git a/src/Annotation/EntityBrowserWidget.php b/src/Annotation/EntityBrowserWidget.php index c192ee0..d8a512e 100644 --- a/src/Annotation/EntityBrowserWidget.php +++ b/src/Annotation/EntityBrowserWidget.php @@ -40,4 +40,11 @@ class EntityBrowserWidget extends Plugin { */ public $description = ''; + /** + * Indicates that widget supports auto selection of entities. + * + * @var bool + */ + public $autoSelect = FALSE; + } diff --git a/src/Form/EntityBrowserForm.php b/src/Form/EntityBrowserForm.php index f2735a0..8f3c1e2 100644 --- a/src/Form/EntityBrowserForm.php +++ b/src/Form/EntityBrowserForm.php @@ -3,6 +3,7 @@ namespace Drupal\entity_browser\Form; use Drupal\Component\Uuid\UuidInterface; +use Drupal\Core\Config\ConfigException; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; @@ -111,6 +112,8 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { $this->init($form_state); } + $this->isFunctionalForm(); + $form['#attributes']['class'][] = 'entity-browser-form'; if (!empty($form_state->get(['entity_browser', 'instance_uuid']))) { $form['#attributes']['data-entity-browser-uuid'] = $form_state->get(['entity_browser', 'instance_uuid']); @@ -145,6 +148,21 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { } /** + * Check if entity browser with selected configuration combination can work. + */ + protected function isFunctionalForm() { + /** @var \Drupal\entity_browser\WidgetInterface $widget */ + foreach ($this->entityBrowser->getWidgets() as $widget) { + /** @var \Drupal\entity_browser\SelectionDisplayInterface $selectionDisplay */ + $selectionDisplay = $this->entityBrowser->getSelectionDisplay(); + + if ($widget->requiresJsCommands() && !$selectionDisplay->supportsJsCommands()) { + throw new ConfigException('Used entity browser selection display cannot work in combination with settings defined for used selection widget.'); + } + } + } + + /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { diff --git a/src/Plugin/EntityBrowser/SelectionDisplay/MultiStepDisplay.php b/src/Plugin/EntityBrowser/SelectionDisplay/MultiStepDisplay.php index 97ac5a7..2609bd3 100644 --- a/src/Plugin/EntityBrowser/SelectionDisplay/MultiStepDisplay.php +++ b/src/Plugin/EntityBrowser/SelectionDisplay/MultiStepDisplay.php @@ -2,6 +2,10 @@ namespace Drupal\entity_browser\Plugin\EntityBrowser\SelectionDisplay; +use Drupal\Core\Ajax\AfterCommand; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\InvokeCommand; +use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\entity_browser\FieldWidgetDisplayManager; @@ -16,7 +20,8 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; * id = "multi_step_display", * label = @Translation("Multi step selection display"), * description = @Translation("Shows the current selection display, allowing to mix elements selected through different widgets in several steps."), - * acceptPreselection = TRUE + * acceptPreselection = TRUE, + * js_commands = TRUE * ) */ class MultiStepDisplay extends SelectionDisplayBase { @@ -80,6 +85,12 @@ class MultiStepDisplay extends SelectionDisplayBase { * {@inheritdoc} */ public function getForm(array &$original_form, FormStateInterface $form_state) { + + // Check if trigger element is dedicated to handle front-end commands. + if (($triggering_element = $form_state->getTriggeringElement()) && $triggering_element['#name'] === 'ajax_commands_handler' && !empty($triggering_element['#value'])) { + $this->executeJsCommand($form_state); + } + $selected_entities = $form_state->get(['entity_browser', 'selected_entities']); $form = []; @@ -115,6 +126,7 @@ class MultiStepDisplay extends SelectionDisplayBase { '#submit' => [[get_class($this), 'removeItemSubmit']], '#name' => 'remove_' . $entity->id() . '_' . $id, '#attributes' => [ + 'class' => ['entity-browser-remove-selected-entity'], 'data-row-id' => $id, 'data-remove-entity' => 'items_' . $entity->id(), ], @@ -126,12 +138,33 @@ class MultiStepDisplay extends SelectionDisplayBase { ], ]; } + + // Add hidden element used to make execution of front-end commands. + $form['ajax_commands_handler'] = [ + '#type' => 'hidden', + '#name' => 'ajax_commands_handler', + '#id' => 'ajax_commands_handler', + '#attributes' => ['id' => 'ajax_commands_handler'], + '#ajax' => [ + 'callback' => [get_class($this), 'handleAjaxCommand'], + 'wrapper' => 'edit-selected', + 'event' => 'execute_js_commands', + 'progress' => [ + 'type' => 'fullscreen', + ], + ], + ]; + $form['use_selected'] = [ '#type' => 'submit', '#value' => $this->t($this->configuration['select_text']), '#name' => 'use_selected', + '#attributes' => [ + 'class' => ['entity-browser-use-selected'], + ], '#access' => empty($selected_entities) ? FALSE : TRUE, ]; + $form['show_selection'] = [ '#type' => 'button', '#value' => $this->t('Show selected'), @@ -145,6 +178,182 @@ class MultiStepDisplay extends SelectionDisplayBase { } /** + * Execute command generated by front-end. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state object. + */ + protected function executeJsCommand(FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + + $commands = json_decode($triggering_element['#value'], TRUE); + + // Process Remove command. + if (isset($commands['remove'])) { + $entity_ids = $commands['remove']; + + // Remove weight of entity being removed. + foreach ($entity_ids as $entity_info) { + $entity_id_info = explode('_', $entity_info['entity_id']); + + $form_state->unsetValue([ + 'selected', + $entity_info['entity_id'], + ]); + + // Remove entity itself. + $selected_entities = &$form_state->get(['entity_browser', 'selected_entities']); + unset($selected_entities[$entity_id_info[2]]); + } + + static::saveNewOrder($form_state); + } + + // Process Add command. + if (isset($commands['add'])) { + $entity_ids = $commands['add']; + + $entities_to_add = []; + $added_entities = []; + + // Generate list of entities grouped by type, to speed up loadMultiple. + foreach ($entity_ids as $entity_pair_info) { + $entity_info = explode(':', $entity_pair_info['entity_id']); + + if (!isset($entities_to_add[$entity_info[0]])) { + $entities_to_add[$entity_info[0]] = []; + } + + $entities_to_add[$entity_info[0]][] = $entity_info[1]; + } + + // Load Entities and add into $added_entities, so that we have list of + // entities with key - "type:id". + foreach ($entities_to_add as $entity_type => $entity_type_ids) { + $indexed_entities = $this->entityTypeManager->getStorage($entity_type) + ->loadMultiple($entity_type_ids); + + foreach ($indexed_entities as $entity_id => $entity) { + $added_entities[implode(':', [ + $entity_type, + $entity_id, + ])] = $entity; + } + } + + // Array is accessed as reference, so that changes are propagated. + $selected_entities = &$form_state->get([ + 'entity_browser', + 'selected_entities', + ]); + + // Fill list of selected entities in correct order with loaded entities. + // In this case, order is preserved and multiple entities with same ID + // can be selected properly. + foreach ($entity_ids as $entity_pair_info) { + $selected_entities[] = $added_entities[$entity_pair_info['entity_id']]; + } + } + } + + /** + * Handler to generate Ajax response, after command is executed. + * + * @param array $form + * Form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state object. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * Return Ajax response with commands. + */ + public static function handleAjaxCommand(array $form, FormStateInterface $form_state) { + $ajax = new AjaxResponse(); + + if (($triggering_element = $form_state->getTriggeringElement()) && $triggering_element['#name'] === 'ajax_commands_handler' && !empty($triggering_element['#value'])) { + $commands = json_decode($triggering_element['#value'], TRUE); + + // Entity IDs that are affected by this command. + if (isset($commands['add'])) { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $entity_ids = $commands['add']; + + $selected_entities = &$form_state->get([ + 'entity_browser', + 'selected_entities', + ]); + + // Get entities added by this command and generate JS commands for them. + $selected_entity_keys = array_keys($selected_entities); + $key_index = count($selected_entity_keys) - count($entity_ids); + foreach ($entity_ids as $entity_pair_info) { + $last_entity_id = $selected_entities[$selected_entity_keys[$key_index]]->id(); + + $html = $renderer->render($form['selection_display']['selected']['items_' . $last_entity_id . '_' . $selected_entity_keys[$key_index]]); + + $ajax->addCommand( + new ReplaceCommand('div[id="' . $entity_pair_info['proxy_id'] . '"]', static::trimSingleHtmlTag($html)) + ); + + $key_index++; + } + + // Check if action buttons should be added to form. When number of added + // entities is equal to number of selected entities. Then form buttons + // should be also rendered: use_selected and show_selection. + if (count($selected_entities) === count($entity_ids)) { + + // Order is important, since commands are executed one after another. + $ajax->addCommand( + new AfterCommand('.entities-list', static::trimSingleHtmlTag($renderer->render($form['selection_display']['show_selection']))) + ); + + $ajax->addCommand( + new AfterCommand('.entities-list', static::trimSingleHtmlTag($renderer->render($form['selection_display']['use_selected']))) + ); + } + } + + // Add Invoke command to trigger loading of entities that are queued + // during execution of current Ajax request. + $ajax->addCommand( + new InvokeCommand('[name=ajax_commands_handler]', 'trigger', ['execute-commands']) + ); + } + + return $ajax; + } + + /** + * Make HTML with single tag suitable for Ajax response. + * + * Comments will be removed and also whitespace characters, because Ajax JS + * "insert" command handling checks number of base elements in response and + * wraps it in a "div" tag if there are more then one base element. + * + * @param string $html + * HTML content. + * + * @return string + * Returns cleaner HTML content, suitable for Ajax responses. + */ + protected static function trimSingleHtmlTag($html) { + $clearHtml = trim($html); + + // Remove comments around main single HTML tag. RegEx flag 's' is there to + // allow matching on whitespaces too. That's needed, because generated HTML + // contains a lot newlines. + if (preg_match_all('/(<(?!(!--)).+((\\/)|(<\\/[a-z]+))>)/is', $clearHtml, $matches)) { + if (!empty($matches) && !empty($matches[0])) { + $clearHtml = $matches[0][0]; + } + } + + return $clearHtml; + } + + /** * Submit callback for remove buttons. * * @param array $form diff --git a/src/Plugin/EntityBrowser/SelectionDisplay/NoDisplay.php b/src/Plugin/EntityBrowser/SelectionDisplay/NoDisplay.php index 05d8843..1a1dbae 100644 --- a/src/Plugin/EntityBrowser/SelectionDisplay/NoDisplay.php +++ b/src/Plugin/EntityBrowser/SelectionDisplay/NoDisplay.php @@ -12,7 +12,8 @@ use Drupal\entity_browser\SelectionDisplayBase; * id = "no_display", * label = @Translation("No selection display"), * description = @Translation("Skips the current selection display and immediately delivers the entities selected."), - * acceptPreselection = FALSE + * acceptPreselection = FALSE, + * js_commands = FALSE * ) */ class NoDisplay extends SelectionDisplayBase { diff --git a/src/Plugin/EntityBrowser/SelectionDisplay/View.php b/src/Plugin/EntityBrowser/SelectionDisplay/View.php index d13dff5..c9722aa 100644 --- a/src/Plugin/EntityBrowser/SelectionDisplay/View.php +++ b/src/Plugin/EntityBrowser/SelectionDisplay/View.php @@ -14,7 +14,8 @@ use Drupal\views\Views; * id = "view", * label = @Translation("View selection display"), * description = @Translation("Use a pre-configured view as selection area."), - * acceptPreselection = TRUE + * acceptPreselection = TRUE, + * js_commands = FALSE * ) */ class View extends SelectionDisplayBase { diff --git a/src/Plugin/EntityBrowser/Widget/Upload.php b/src/Plugin/EntityBrowser/Widget/Upload.php index e05e9a2..9b5493c 100644 --- a/src/Plugin/EntityBrowser/Widget/Upload.php +++ b/src/Plugin/EntityBrowser/Widget/Upload.php @@ -19,7 +19,8 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; * @EntityBrowserWidget( * id = "upload", * label = @Translation("Upload"), - * description = @Translation("Adds an upload field browser's widget.") + * description = @Translation("Adds an upload field browser's widget."), + * auto_select = FALSE * ) */ class Upload extends WidgetBase { @@ -162,6 +163,8 @@ class Upload extends WidgetBase { * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + $form['upload_location'] = [ '#type' => 'textfield', '#title' => $this->t('Upload location'), @@ -182,12 +185,6 @@ class Upload extends WidgetBase { '#required' => TRUE, ]; - $form['submit_text'] = [ - '#type' => 'textfield', - '#title' => $this->t('Submit button text'), - '#default_value' => $this->configuration['submit_text'], - ]; - if ($this->moduleHandler->moduleExists('token')) { $form['token_help'] = [ '#theme' => 'token_tree_link', diff --git a/src/Plugin/EntityBrowser/Widget/View.php b/src/Plugin/EntityBrowser/Widget/View.php index 8e78276..e9794c0 100644 --- a/src/Plugin/EntityBrowser/Widget/View.php +++ b/src/Plugin/EntityBrowser/Widget/View.php @@ -22,7 +22,8 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; * id = "view", * label = @Translation("View"), * provider = "views", - * description = @Translation("Uses a view to provide entity listing in a browser's widget.") + * description = @Translation("Uses a view to provide entity listing in a browser's widget."), + * auto_select = TRUE * ) */ class View extends WidgetBase implements ContainerFactoryPluginInterface { @@ -257,6 +258,7 @@ class View extends WidgetBase implements ContainerFactoryPluginInterface { public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { $values = $form_state->getValues()['table'][$this->uuid()]['form']; $this->configuration['submit_text'] = $values['submit_text']; + $this->configuration['auto_select'] = $values['auto_select']; if (!empty($values['view'])) { list($view_id, $display_id) = explode('.', $values['view']); $this->configuration['view'] = $view_id; diff --git a/src/SelectionDisplayBase.php b/src/SelectionDisplayBase.php index b91c79d..c48137a 100644 --- a/src/SelectionDisplayBase.php +++ b/src/SelectionDisplayBase.php @@ -132,6 +132,13 @@ abstract class SelectionDisplayBase extends PluginBase implements SelectionDispl } /** + * {@inheritdoc} + */ + public function supportsJsCommands() { + return $this->getPluginDefinition()['js_commands']; + } + + /** * Marks selection as done - sets value in form state and dispatches event. */ protected function selectionDone(FormStateInterface $form_state) { diff --git a/src/SelectionDisplayInterface.php b/src/SelectionDisplayInterface.php index a072a52..84e1564 100644 --- a/src/SelectionDisplayInterface.php +++ b/src/SelectionDisplayInterface.php @@ -69,4 +69,12 @@ interface SelectionDisplayInterface extends PluginInspectionInterface, Configura */ public function checkPreselectionSupport(); + /** + * Returns true if selection display supports selection over javascript. + * + * @return bool + * True if javascript add/remove events are supported. + */ + public function supportsJsCommands(); + } diff --git a/src/Tests/ConfigUITest.php b/src/Tests/ConfigUITest.php index 3ddeceb..da58a79 100644 --- a/src/Tests/ConfigUITest.php +++ b/src/Tests/ConfigUITest.php @@ -218,7 +218,12 @@ class ConfigUITest extends WebTestBase { $this->assertEqual('entity_form', $widget->id(), 'Entity browser widget was correctly saved.'); $this->assertEqual($second_uuid, $widget->uuid(), 'Entity browser widget uuid was correctly saved.'); $configuration = $widget->getConfiguration()['settings']; - $this->assertEqual(['entity_type' => 'user', 'bundle' => 'user', 'form_mode' => 'register', 'submit_text' => 'But some are more equal than others'], $configuration, 'Entity browser widget configuration was correctly saved.'); + $this->assertEqual([ + 'entity_type' => 'user', + 'bundle' => 'user', + 'form_mode' => 'register', + 'submit_text' => 'But some are more equal than others', + ], $configuration, 'Entity browser widget configuration was correctly saved.'); $this->assertEqual(2, $widget->getWeight(), 'Entity browser widget weight was correctly saved.'); // Navigate to edit. diff --git a/src/WidgetBase.php b/src/WidgetBase.php index d4b9b94..0916751 100644 --- a/src/WidgetBase.php +++ b/src/WidgetBase.php @@ -109,6 +109,8 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain * {@inheritdoc} */ public function getForm(array &$original_form, FormStateInterface $form_state, array $additional_widget_parameters) { + $form = []; + // Allow configuration overrides at runtime based on form state to enable // use cases where the instance of a widget may have contextual // configuration like field settings. "widget_context" doesn't have to be @@ -120,30 +122,46 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain } } - $form['actions'] = [ - '#type' => 'actions', - 'submit' => [ - '#type' => 'submit', - '#value' => $this->configuration['submit_text'], - '#eb_widget_main_submit' => TRUE, - '#attributes' => ['class' => ['is-entity-browser-submit']], - '#button_type' => 'primary', - ], - ]; + // Check if widget supports auto select functionality and expose config to + // front-end javascript. + $autoSelect = FALSE; + if ($this->getPluginDefinition()['auto_select']) { + $autoSelect = $this->configuration['auto_select']; + $form['#attached']['drupalSettings']['entity_browser_widget']['auto_select'] = $autoSelect; + } + + // In case of auto select, widget will handle adding entities in JS. + if (!$autoSelect) { + $form['actions'] = [ + '#type' => 'actions', + 'submit' => [ + '#type' => 'submit', + '#value' => $this->configuration['submit_text'], + '#eb_widget_main_submit' => TRUE, + '#attributes' => ['class' => ['is-entity-browser-submit']], + '#button_type' => 'primary', + ], + ]; + } return $form; } - /** * {@inheritdoc} */ public function defaultConfiguration() { - return [ + $defaultConfig = [ 'submit_text' => $this->t('Select entities'), ]; - } + // If auto select is supported by Widget, append default configuration. + if ($this->getPluginDefinition()['auto_select']) { + $defaultConfig['auto_select'] = FALSE; + } + + return $defaultConfig; + } /** * {@inheritdoc} @@ -197,6 +215,15 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain '#default_value' => $this->configuration['submit_text'], ]; + // Allow "auto_select" setting when auto_select is supported by widget. + if ($this->getPluginDefinition()['auto_select']) { + $form['auto_select'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Automatically submit selection'), + '#default_value' => $this->configuration['auto_select'], + ]; + } + return $form; } @@ -323,4 +350,11 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain )); } + /** + * {@inheritdoc} + */ + public function requiresJsCommands() { + return $this->getPluginDefinition()['auto_select'] && $this->getConfiguration()['settings']['auto_select']; + } + } diff --git a/src/WidgetInterface.php b/src/WidgetInterface.php index 861033e..0a1140f 100644 --- a/src/WidgetInterface.php +++ b/src/WidgetInterface.php @@ -109,4 +109,13 @@ interface WidgetInterface extends PluginInspectionInterface, ConfigurablePluginI */ public function submit(array &$element, array &$form, FormStateInterface $form_state); + /** + * Returns if widget requires JS commands support by selection display. + * + * @return bool + * True is auto selection is enabled and add/remove of entities will be done + * over javascript events on selection display. + */ + public function requiresJsCommands(); + } diff --git a/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_grid.yml b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_grid.yml new file mode 100644 index 0000000..4610a12 --- /dev/null +++ b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_grid.yml @@ -0,0 +1,413 @@ +langcode: en +status: true +dependencies: + module: + - entity_browser + - file + - user +id: files_entity_browser_grid +label: 'Files entity browser Grid' +module: views +description: '' +tag: '' +base_table: file_managed +base_field: fid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: grid + options: + grouping: { } + columns: 2 + automatic_width: true + alignment: horizontal + col_class_default: true + col_class_custom: '' + row_class_default: true + row_class_custom: '' + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + entity_browser_select: + id: entity_browser_select + table: file_managed + field: entity_browser_select + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: null + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + entity_type: file + plugin_id: entity_browser_select + filename: + id: filename + table: file_managed + field: filename + entity_type: file + entity_field: filename + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + link_to_file: true + plugin_id: file + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + created: + id: created + table: file_managed + field: created + relationship: none + group_type: group + admin_label: '' + label: Created + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: null + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + date_format: fallback + custom_date_format: '' + timezone: '' + entity_type: file + entity_field: created + plugin_id: date + filesize: + id: filesize + table: file_managed + field: filesize + relationship: none + group_type: group + admin_label: '' + label: 'File size' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: null + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + file_size_display: formatted + entity_type: file + entity_field: filesize + plugin_id: file_size + status: + id: status + table: file_managed + field: status + relationship: none + group_type: group + admin_label: '' + label: Status + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: null + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + entity_type: file + entity_field: status + plugin_id: file_status + filters: + filename: + id: filename + table: file_managed + field: filename + relationship: none + group_type: group + admin_label: '' + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: filename_op + label: Filename + description: '' + use_operator: false + operator: filename_op + identifier: filename + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: file + entity_field: filename + plugin_id: string + sorts: + created: + id: created + table: file_managed + field: created + order: DESC + entity_type: file + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } + entity_browser_1: + display_plugin: entity_browser + id: entity_browser_1 + display_title: 'Entity browser' + position: 2 + display_options: + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } diff --git a/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_html.yml b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_html.yml new file mode 100644 index 0000000..7a1586d --- /dev/null +++ b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_html.yml @@ -0,0 +1,411 @@ +langcode: en +status: true +dependencies: + module: + - entity_browser + - file + - user +id: files_entity_browser_html +label: 'Files entity browser HTML' +module: views +description: '' +tag: '' +base_table: file_managed +base_field: fid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: html_list + options: + grouping: { } + row_class: '' + default_row_class: true + type: ul + wrapper_class: item-list + class: '' + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + entity_browser_select: + id: entity_browser_select + table: file_managed + field: entity_browser_select + relationship: none + group_type: group + admin_label: '' + label: 'Entity browser bulk select form' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + entity_type: file + plugin_id: entity_browser_select + filename: + id: filename + table: file_managed + field: filename + entity_type: file + entity_field: filename + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + link_to_file: true + plugin_id: file + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + created: + id: created + table: file_managed + field: created + relationship: none + group_type: group + admin_label: '' + label: Created + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: null + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + date_format: fallback + custom_date_format: '' + timezone: '' + entity_type: file + entity_field: created + plugin_id: date + filesize: + id: filesize + table: file_managed + field: filesize + relationship: none + group_type: group + admin_label: '' + label: 'File size' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: null + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + file_size_display: formatted + entity_type: file + entity_field: filesize + plugin_id: file_size + status: + id: status + table: file_managed + field: status + relationship: none + group_type: group + admin_label: '' + label: Status + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: null + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + entity_type: file + entity_field: status + plugin_id: file_status + filters: + filename: + id: filename + table: file_managed + field: filename + relationship: none + group_type: group + admin_label: '' + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: filename_op + label: Filename + description: '' + use_operator: false + operator: filename_op + identifier: filename + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: file + entity_field: filename + plugin_id: string + sorts: + created: + id: created + table: file_managed + field: created + order: DESC + entity_type: file + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } + entity_browser_1: + display_plugin: entity_browser + id: entity_browser_1 + display_title: 'Entity browser' + position: 1 + display_options: + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } diff --git a/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_unformatted.yml b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_unformatted.yml new file mode 100644 index 0000000..7792d65 --- /dev/null +++ b/tests/modules/entity_browser_test/config/install/views.view.files_entity_browser_unformatted.yml @@ -0,0 +1,408 @@ +langcode: en +status: true +dependencies: + module: + - entity_browser + - file + - user +id: files_entity_browser_unformatted +label: 'Files entity browser Unformatted' +module: views +description: '' +tag: '' +base_table: file_managed +base_field: fid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + entity_browser_select: + id: entity_browser_select + table: file_managed + field: entity_browser_select + relationship: none + group_type: group + admin_label: '' + label: 'Entity browser bulk select form' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + entity_type: file + plugin_id: entity_browser_select + filename: + id: filename + table: file_managed + field: filename + entity_type: file + entity_field: filename + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + link_to_file: true + plugin_id: file + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + created: + id: created + table: file_managed + field: created + relationship: none + group_type: group + admin_label: '' + label: Created + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: null + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + date_format: fallback + custom_date_format: '' + timezone: '' + entity_type: file + entity_field: created + plugin_id: date + filesize: + id: filesize + table: file_managed + field: filesize + relationship: none + group_type: group + admin_label: '' + label: 'File size' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: null + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + file_size_display: formatted + entity_type: file + entity_field: filesize + plugin_id: file_size + status: + id: status + table: file_managed + field: status + relationship: none + group_type: group + admin_label: '' + label: Status + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: null + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + entity_type: file + entity_field: status + plugin_id: file_status + filters: + filename: + id: filename + table: file_managed + field: filename + relationship: none + group_type: group + admin_label: '' + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: filename_op + label: Filename + description: '' + use_operator: false + operator: filename_op + identifier: filename + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: file + entity_field: filename + plugin_id: string + sorts: + created: + id: created + table: file_managed + field: created + order: DESC + entity_type: file + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } + entity_browser_1: + display_plugin: entity_browser + id: entity_browser_1 + display_title: 'Entity browser' + position: 1 + display_options: + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } diff --git a/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/DummyWidget.php b/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/DummyWidget.php index 345609a..7d19edf 100644 --- a/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/DummyWidget.php +++ b/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/DummyWidget.php @@ -11,7 +11,8 @@ use Drupal\entity_browser\WidgetBase; * @EntityBrowserWidget( * id = "dummy", * label = @Translation("Dummy widget"), - * description = @Translation("Dummy widget existing for testing purposes.") + * description = @Translation("Dummy widget existing for testing purposes."), + * auto_select = FALSE * ) */ class DummyWidget extends WidgetBase { diff --git a/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/MultipleSubmitTestWidget.php b/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/MultipleSubmitTestWidget.php index 51a982e..8c9db76 100644 --- a/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/MultipleSubmitTestWidget.php +++ b/tests/modules/entity_browser_test/src/Plugin/EntityBrowser/Widget/MultipleSubmitTestWidget.php @@ -11,7 +11,8 @@ use Drupal\entity_browser\WidgetBase; * @EntityBrowserWidget( * id = "multiple_submit_test_widget", * label = @Translation("Multiple submit test widget"), - * description = @Translation("Test widget with multiple submit buttons only for testing purposes.") + * description = @Translation("Test widget with multiple submit buttons only for testing purposes."), + * auto_select = FALSE * ) */ class MultipleSubmitTestWidget extends WidgetBase { diff --git a/tests/src/FunctionalJavascript/EntityBrowserJavascriptTestBase.php b/tests/src/FunctionalJavascript/EntityBrowserJavascriptTestBase.php index 3442f26..568a595 100644 --- a/tests/src/FunctionalJavascript/EntityBrowserJavascriptTestBase.php +++ b/tests/src/FunctionalJavascript/EntityBrowserJavascriptTestBase.php @@ -2,14 +2,12 @@ namespace Drupal\Tests\entity_browser\FunctionalJavascript; -use Drupal\Component\Utility\SafeMarkup; -use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\file\Entity\File; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\FunctionalJavascriptTests\JavascriptTestBase; -use Drupal\Tests\Component\Utility\SafeMarkupTest; /** * Base class for Entity browser Javascript functional tests. @@ -108,11 +106,13 @@ abstract class EntityBrowserJavascriptTestBase extends JavascriptTestBase { * The widget selector configuration. * @param array $selection_display_configuration * The selection display configuration. + * @param array $widget_configurations + * Widget configurations. Have be provided with widget UUIDs. * * @return \Drupal\entity_browser\EntityBrowserInterface * Returns an Entity Browser. */ - protected function getEntityBrowser($browser_name, $display_id, $widget_selector_id, $selection_display_id, $display_configuration = [], $widget_selector_configuration = [], $selection_display_configuration = []) { + protected function getEntityBrowser($browser_name, $display_id, $widget_selector_id, $selection_display_id, array $display_configuration = [], array $widget_selector_configuration = [], array $selection_display_configuration = [], array $widget_configurations = []) { /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ $storage = $this->container->get('entity_type.manager') ->getStorage('entity_browser'); @@ -136,6 +136,15 @@ abstract class EntityBrowserJavascriptTestBase extends JavascriptTestBase { $browser->getSelectionDisplay() ->setConfiguration($selection_display_configuration); } + + // Apply custom widget configurations. + if ($widget_configurations) { + foreach ($widget_configurations as $widget_uuid => $widget_config) { + $view_widget = $browser->getWidget($widget_uuid); + $view_widget->setConfiguration(NestedArray::mergeDeep($view_widget->getConfiguration(), $widget_config)); + } + } + $browser->save(); // Clear caches after new browser is saved to remove old cached states. @@ -191,6 +200,22 @@ abstract class EntityBrowserJavascriptTestBase extends JavascriptTestBase { } /** + * Click on element found by xpath selector. + * + * @param string $xpathSelector + * Xpath selector for element that will be used to trigger click on it. + * @param bool $waitAfterAction + * Flag to wait after click is executed. + */ + protected function clickXpathSelector($xpathSelector, $waitAfterAction = TRUE) { + $this->getSession()->getPage()->find('xpath', $xpathSelector)->click(); + + if ($waitAfterAction) { + $this->waitForAjaxToFinish(); + } + } + + /** * Debugger method to save additional HTML output. * * The base class will only save browser output when accessing page using @@ -210,5 +235,4 @@ abstract class EntityBrowserJavascriptTestBase extends JavascriptTestBase { } } - } diff --git a/tests/src/FunctionalJavascript/MultiStepSelectionDisplayTest.php b/tests/src/FunctionalJavascript/MultiStepSelectionDisplayTest.php new file mode 100644 index 0000000..bfbd06a --- /dev/null +++ b/tests/src/FunctionalJavascript/MultiStepSelectionDisplayTest.php @@ -0,0 +1,253 @@ +getSession()->getPage()->clickLink('Select entities'); + $this->getSession() + ->switchToIFrame('entity_browser_iframe_test_entity_browser_file'); + $this->waitForAjaxToFinish(); + } + + /** + * Close iframe entity browser and change scope to base page. + */ + protected function closeEntityBrowser() { + $this->clickXpathSelector('//*[@data-drupal-selector="edit-use-selected"]'); + $this->getSession()->switchToIFrame(); + $this->waitForAjaxToFinish(); + } + + /** + * Click on entity in view to be selected. + * + * @param string $entityId + * Entity ID that will be selected. Format: "file:1". + */ + protected function clickViewEntity($entityId) { + $xpathViewRow = '//*[./*[contains(@class, "views-field-entity-browser-select") and .//input[@name="entity_browser_select[' . $entityId . ']"]]]'; + + $this->clickXpathSelector($xpathViewRow, FALSE); + } + + /** + * Wait for Ajax Commands to finish. + * + * Since commands are executed in batches, it can occur that one command is + * still running and new one will be collected for next batch. To ensure all + * of commands are executed, we have to add additional 200ms wait, before next + * batch is triggered. + * + * It's related to: Drupal.entityBrowserCommandQueue.executeCommands + */ + protected function waitSelectionDisplayAjaxCommands() { + $this->waitForAjaxToFinish(); + $this->getSession()->wait(200); + $this->waitForAjaxToFinish(); + } + + /** + * Change selection mode for article reference field form display widget. + * + * @param array $configuration + * Configuration that will be used for field form display. + */ + protected function changeFieldFormDisplayConfig(array $configuration) { + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */ + $form_display = $this->container->get('entity_type.manager') + ->getStorage('entity_form_display') + ->load('node.article.default'); + + $form_display->setComponent( + 'field_reference', + NestedArray::mergeDeep($form_display->getComponent('field_reference'), $configuration) + )->save(); + } + + /** + * Check that selection state in entity browser Inline Entity Form. + */ + public function testAjaxCommands() { + + $this->createFile('test_file1'); + $this->createFile('test_file2'); + $this->createFile('test_file3'); + + // Testing Action buttons (adding and removing) with usage of HTML View. + $widget_configurations = [ + // View widget configuration. + '774798f1-5ec5-4b63-84bd-124cd51ec07d' => [ + 'settings' => [ + 'view' => 'files_entity_browser_html', + 'auto_select' => TRUE, + ], + ], + ]; + $this->getEntityBrowser('test_entity_browser_file', 'iframe', 'tabs', 'multi_step_display', [], [], [], $widget_configurations); + $this->drupalGet('node/add/article'); + $this->openEntityBrowser(); + + // Check that action buttons are not there. + $this->assertSession() + ->elementNotExists('xpath', '//*[@data-drupal-selector="edit-use-selected"]'); + $this->assertSession() + ->elementNotExists('xpath', '//*[@data-drupal-selector="edit-show-selection"]'); + + $this->clickViewEntity('file:1'); + $this->waitSelectionDisplayAjaxCommands(); + + // Check that action buttons are there. + $this->assertSession() + ->elementExists('xpath', '//*[@data-drupal-selector="edit-use-selected"]'); + $this->assertSession() + ->elementExists('xpath', '//*[@data-drupal-selector="edit-show-selection"]'); + + // Click on first entity Remove button. + $this->clickXpathSelector('//input[@data-row-id="0"]'); + $this->waitSelectionDisplayAjaxCommands(); + + // Check that action buttons are not there. + $this->assertSession() + ->elementNotExists('xpath', '//*[@data-drupal-selector="edit-use-selected"]'); + $this->assertSession() + ->elementNotExists('xpath', '//*[@data-drupal-selector="edit-show-selection"]'); + + $this->clickViewEntity('file:1'); + $this->waitSelectionDisplayAjaxCommands(); + $this->closeEntityBrowser(); + + // Testing quick adding and removing of entities with usage of Table + // (default) view. + $widget_configurations = [ + // View widget configuration. + '774798f1-5ec5-4b63-84bd-124cd51ec07d' => [ + 'settings' => [ + 'view' => 'files_entity_browser', + 'auto_select' => TRUE, + ], + ], + ]; + $this->getEntityBrowser('test_entity_browser_file', 'iframe', 'tabs', 'multi_step_display', [], [], [], $widget_configurations); + $this->drupalGet('node/add/article'); + $this->openEntityBrowser(); + + // Quickly add 5 entities. + $entitiesToAdd = ['file:1', 'file:2', 'file:3', 'file:1', 'file:2']; + foreach ($entitiesToAdd as $entityId) { + $this->clickViewEntity($entityId); + } + $this->waitSelectionDisplayAjaxCommands(); + + // Check that there are 5 entities in selection display list. + $this->assertSession() + ->elementsCount('xpath', '//div[contains(@class, "entities-list")]/*', 5); + + // Quickly remove all 5 entities. + foreach (array_keys($entitiesToAdd) as $entityIndex) { + $this->clickXpathSelector('//input[@data-row-id="' . $entityIndex . '"]'); + } + $this->waitSelectionDisplayAjaxCommands(); + + // Check that action buttons are not there. + $this->assertSession() + ->elementNotExists('xpath', '//*[@data-drupal-selector="edit-use-selected"]'); + $this->assertSession() + ->elementNotExists('xpath', '//*[@data-drupal-selector="edit-show-selection"]'); + + $this->clickViewEntity('file:1'); + $this->waitSelectionDisplayAjaxCommands(); + $this->closeEntityBrowser(); + + // Testing adding with preselection with usage of Grid view. + $widget_configurations = [ + // View widget configuration. + '774798f1-5ec5-4b63-84bd-124cd51ec07d' => [ + 'settings' => [ + 'view' => 'files_entity_browser_grid', + 'auto_select' => TRUE, + ], + ], + ]; + $this->getEntityBrowser('test_entity_browser_file', 'iframe', 'tabs', 'multi_step_display', [], [], [], $widget_configurations); + + // Change selection mode to 'Edit', to test adding/removing inside EB. + $this->changeFieldFormDisplayConfig([ + 'settings' => [ + 'selection_mode' => 'selection_edit', + ], + ]); + + $this->drupalGet('node/add/article'); + $this->openEntityBrowser(); + + $this->clickViewEntity('file:1'); + $this->waitSelectionDisplayAjaxCommands(); + $this->closeEntityBrowser(); + + $this->openEntityBrowser(); + + $this->clickViewEntity('file:2'); + $this->waitSelectionDisplayAjaxCommands(); + $this->closeEntityBrowser(); + + $this->assertSession() + ->elementsCount('xpath', '//div[contains(@class, "entities-list")]/*', 2); + + // Testing removing with preselection with usage of Unformatted view. + $widget_configurations = [ + // View widget configuration. + '774798f1-5ec5-4b63-84bd-124cd51ec07d' => [ + 'settings' => [ + 'view' => 'files_entity_browser_unformatted', + 'auto_select' => TRUE, + ], + ], + ]; + $this->getEntityBrowser('test_entity_browser_file', 'iframe', 'tabs', 'multi_step_display', [], [], [], $widget_configurations); + + $this->drupalGet('node/add/article'); + $this->openEntityBrowser(); + + // Select 3 entities. + $entitiesToAdd = ['file:1', 'file:2', 'file:3']; + foreach ($entitiesToAdd as $entityId) { + $this->clickViewEntity($entityId); + + // For some reason PhantomJS crashes here on quick clicking. That's why + // waiting is added. Selenium works fine. + $this->waitSelectionDisplayAjaxCommands(); + } + $this->closeEntityBrowser(); + + // Check that there are 3 entities in selection list after closing of EB. + $this->assertSession() + ->elementsCount('xpath', '//div[contains(@class, "entities-list")]/*', 3); + + $this->openEntityBrowser(); + + // Click on first entity Remove button. + $this->clickXpathSelector('//input[@data-row-id="0"]'); + $this->waitSelectionDisplayAjaxCommands(); + + $this->closeEntityBrowser(); + + // Check that there are 2 entities in selection list after closing of EB. + $this->assertSession() + ->elementsCount('xpath', '//div[contains(@class, "entities-list")]/*', 2); + } + +} diff --git a/tests/src/Kernel/Extension/EntityBrowserTest.php b/tests/src/Kernel/Extension/EntityBrowserTest.php index 0c33bf6..9a49df0 100644 --- a/tests/src/Kernel/Extension/EntityBrowserTest.php +++ b/tests/src/Kernel/Extension/EntityBrowserTest.php @@ -178,6 +178,7 @@ class EntityBrowserTest extends KernelTestBase { 'view' => 'test_view', 'view_display' => 'test_display', 'submit_text' => 'Select entities', + 'auto_select' => FALSE, ], ], ],