diff --git a/config/schema/entity_browser.schema.yml b/config/schema/entity_browser.schema.yml index 3d1e662..6d5c604 100644 --- a/config/schema/entity_browser.schema.yml +++ b/config/schema/entity_browser.schema.yml @@ -91,6 +91,9 @@ entity_browser.browser.widget.upload: submit_text: type: string label: 'Submit button text' + auto_select: + type: boolean + label: 'Automatically submit selection' upload_location: type: string label: 'Upload location' @@ -102,6 +105,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..9a62138 --- /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 commandName + * Command name, that will be executed. + * @param 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)); \ No newline at end of file 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..45c68ec 100644 --- a/js/entity_browser.view.js +++ b/js/entity_browser.view.js @@ -47,6 +47,29 @@ } }); }); + + // 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) { + views_instance.$view.find('tr') + .once('register-row-click') + .click(function (event) { + event.preventDefault(); + + var $row = $(this); + var $input = $row.find('input'); + + $row.parents('form') + .find('.entities-list') + .trigger('add-entities', [[$input.val()]]); + }); + + // Hide column with checkboxes. + views_instance.$view.find('.views-field-entity-browser-select') + .hide(); + } } } }; diff --git a/modules/entity_form/config/schema/entity_browser_entity_form.schema.yml b/modules/entity_form/config/schema/entity_browser_entity_form.schema.yml index 16928a1..0985f59 100644 --- a/modules/entity_form/config/schema/entity_browser_entity_form.schema.yml +++ b/modules/entity_form/config/schema/entity_browser_entity_form.schema.yml @@ -13,6 +13,9 @@ entity_browser.browser.widget.entity_form: submit_text: type: string label: 'Submit button text' + auto_select: + type: boolean + label: 'Automatically submit selection' entity_type: type: string label: 'Entity type ID' diff --git a/modules/entity_form/src/Plugin/EntityBrowser/Widget/EntityForm.php b/modules/entity_form/src/Plugin/EntityBrowser/Widget/EntityForm.php index 05676cf..90fe0b0 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."), + * autoSelect = 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..76338b8 --- /dev/null +++ b/modules/example/config/install/entity_browser.browser.test_files_ajax.yml @@ -0,0 +1,45 @@ +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' + auto_select: false + 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..a729234 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 $jsCommands = 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 ac290da..dad7127 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; @@ -75,7 +76,7 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { /** * Initializes form state. * - * @param \Drupal\Core\Form\FormStateInterface + * @param \Drupal\Core\Form\FormStateInterface $form_state * Form state object. */ protected function init(FormStateInterface $form_state) { @@ -109,6 +110,8 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { $this->init($form_state); } + $this->isFunctionalForm($form_state); + $form['#attributes']['class'][] = 'entity-browser-form'; $form['#browser_parts'] = [ 'widget_selector' => 'widget_selector', @@ -140,6 +143,25 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { } /** + * Check if entity browser with selected configuration combination can work. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form status. + */ + protected function isFunctionalForm(FormStateInterface $form_state) { + /** @var \Drupal\entity_browser\WidgetInterface $widget */ + $widget = $this->entity_browser->getWidgets() + ->get($this->getCurrentWidget($form_state)); + + /** @var \Drupal\entity_browser\SelectionDisplayInterface $selectionDisplay */ + $selectionDisplay = $this->entity_browser->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..39abd3b 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, + * jsCommands = 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,153 @@ 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'] . '"]', trim($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', trim($renderer->render($form['selection_display']['show_selection']))) + ); + + $ajax->addCommand( + new AfterCommand('.entities-list', trim($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; + } + + /** * 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 7770166..3b9e468 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, + * jsCommands = FALSE * ) */ class NoDisplay extends SelectionDisplayBase { diff --git a/src/Plugin/EntityBrowser/SelectionDisplay/View.php b/src/Plugin/EntityBrowser/SelectionDisplay/View.php index d13dff5..8001cda 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, + * jsCommands = FALSE * ) */ class View extends SelectionDisplayBase { diff --git a/src/Plugin/EntityBrowser/Widget/Upload.php b/src/Plugin/EntityBrowser/Widget/Upload.php index e65dc86..3e0b13a 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."), + * autoSelect = FALSE * ) */ class Upload extends WidgetBase { @@ -166,6 +167,14 @@ class Upload extends WidgetBase { '#default_value' => $this->configuration['submit_text'], ]; + // Allow "auto_select" setting when autoSelect is supported by widget. + $form['auto_select'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Automatically submit selection'), + '#default_value' => $this->configuration['auto_select'], + '#disabled' => !$this->getPluginDefinition()['autoSelect'], + ]; + 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..1d561d8 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."), + * autoSelect = 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..218bd94 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()['jsCommands']; + } + + /** * 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 ae13cfd..c00859b 100644 --- a/src/Tests/ConfigUITest.php +++ b/src/Tests/ConfigUITest.php @@ -207,13 +207,31 @@ class ConfigUITest extends WebTestBase { $this->assertEqual('upload', $widget->id(), 'Entity browser widget was correctly saved.'); $this->assertEqual($first_uuid, $widget->uuid(), 'Entity browser widget uuid was correctly saved.'); $configuration = $widget->getConfiguration()['settings']; - $this->assertEqual(['upload_location' => 'public://', 'submit_text' => 'Select files'], $configuration, 'Entity browser widget configuration was correctly saved.'); + $this->assertEqual( + [ + 'upload_location' => 'public://', + 'submit_text' => 'Select files', + 'auto_select' => FALSE, + ], + $configuration, + 'Entity browser widget configuration was correctly saved.' + ); $this->assertEqual(1, $widget->getWeight(), 'Entity browser widget weight was correctly saved.'); $widget = $widgets->get($second_uuid); $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', + 'auto_select' => FALSE, + ], + $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 a7c8fb7..d78e532 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,33 @@ 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']], - ], - ]; + // In case of auto submitting, widget will handle adding entities in JS. + $form['#attached']['drupalSettings']['entity_browser_widget']['auto_select'] = $this->configuration['auto_select']; + if (!$this->configuration['auto_select']) { + $form['actions'] = [ + '#type' => 'actions', + 'submit' => [ + '#type' => 'submit', + '#value' => $this->configuration['submit_text'], + '#eb_widget_main_submit' => TRUE, + '#attributes' => ['class' => ['is-entity-browser-submit']], + ], + ]; + } return $form; } - /** * {@inheritdoc} */ public function defaultConfiguration() { return [ 'submit_text' => $this->t('Select entities'), + 'auto_select' => FALSE, ]; } - /** * {@inheritdoc} */ @@ -196,6 +201,14 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain '#default_value' => $this->configuration['submit_text'], ]; + // Allow "auto_select" setting when autoSelect is supported by widget. + $form['auto_select'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Automatically submit selection'), + '#default_value' => $this->configuration['auto_select'], + '#disabled' => !$this->getPluginDefinition()['autoSelect'], + ]; + return $form; } @@ -322,4 +335,11 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain )); } + /** + * {@inheritdoc} + */ + public function requiresJsCommands() { + return $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/schema/entity_browser_test.schema.yml b/tests/modules/entity_browser_test/config/schema/entity_browser_test.schema.yml index 3eb0256..f5370f9 100644 --- a/tests/modules/entity_browser_test/config/schema/entity_browser_test.schema.yml +++ b/tests/modules/entity_browser_test/config/schema/entity_browser_test.schema.yml @@ -5,6 +5,9 @@ entity_browser.browser.widget.dummy: submit_text: type: string label: 'Submit button text' + auto_select: + type: boolean + label: 'Automatically submit selection' text: type: string label: 'Text' 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..f7f62a5 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."), + * autoSelect = FALSE * ) */ class DummyWidget extends WidgetBase { diff --git a/tests/src/Kernel/Extension/EntityBrowserTest.php b/tests/src/Kernel/Extension/EntityBrowserTest.php index f49ef02..5829382 100644 --- a/tests/src/Kernel/Extension/EntityBrowserTest.php +++ b/tests/src/Kernel/Extension/EntityBrowserTest.php @@ -166,6 +166,7 @@ class EntityBrowserTest extends KernelTestBase { 'view' => 'test_view', 'view_display' => 'test_display', 'submit_text' => 'Select entities', + 'auto_select' => FALSE, ], ], ],