From c5f240dbf0b226e3349990eb72402c7018c012a3 Mon Sep 17 00:00:00 2001 From: Geoffrey Roberts <15257-geoffreyr@users.noreply.drupalcode.org> Date: Thu, 14 Nov 2024 05:41:41 +0000 Subject: [PATCH] Issue #3059845: Update branch to match latest 2.x-dev and add support for inline widget display compatible with Drupal 10 --- css/entity-reference-tree-inline.css | 16 ++ entity_reference_tree.libraries.yml | 11 + js/entity_reference_tree_inline.js | 180 ++++++++++++++++ .../InlineEntityReferenceTreeWidget.php | 203 ++++++++++++++++++ 4 files changed, 410 insertions(+) create mode 100644 css/entity-reference-tree-inline.css create mode 100644 js/entity_reference_tree_inline.js create mode 100644 src/Plugin/Field/FieldWidget/InlineEntityReferenceTreeWidget.php diff --git a/css/entity-reference-tree-inline.css b/css/entity-reference-tree-inline.css new file mode 100644 index 0000000..4290d59 --- /dev/null +++ b/css/entity-reference-tree-inline.css @@ -0,0 +1,16 @@ +/* Reduce vertical gap between heading and search box. */ +.field--widget-inline-entity-reference-tree-widget .claro-autocomplete { + display: block; +} + +/* Reduce vertical margins between heading and search box. */ +.inline-entity-reference-tree-wrapper .form-type--textfield { + margin-top: 0.7em; + margin-bottom: 0.7em; +} + +/* Bold 'selected' text line with some margin above. */ +.inline-entity-reference-tree-wrapper .entity-reference-tree-selected-text { + font-weight: 600; + margin-top: 0.5em; +} diff --git a/entity_reference_tree.libraries.yml b/entity_reference_tree.libraries.yml index b635728..3bc4984 100644 --- a/entity_reference_tree.libraries.yml +++ b/entity_reference_tree.libraries.yml @@ -32,3 +32,14 @@ entity_tree: dependencies: - entity_reference_tree/jstree + +entity_tree_inline: + js: + js/entity_reference_tree_inline.js: {} + css: + theme: + css/entity-reference-tree-inline.css: {} + dependencies: + - core/jquery + - entity_reference_tree/widget + - entity_reference_tree/jstree diff --git a/js/entity_reference_tree_inline.js b/js/entity_reference_tree_inline.js new file mode 100644 index 0000000..ad15469 --- /dev/null +++ b/js/entity_reference_tree_inline.js @@ -0,0 +1,180 @@ +/** + * @file + * Entity Reference Tree JavaScript file. + */ + +// Codes run both on normal page loads and when data is loaded by AJAX (or BigPipe!) +// @See https://www.drupal.org/docs/8/api/javascript-api/javascript-api-overview +(function ($, Drupal, once) { + Drupal.behaviors.entityReferenceTreeInline = { + attach: function (context, settings) { + const entityJSTreeInline = once('jstreeBehavior', '.inline-entity-reference-tree-wrapper', context); + entityJSTreeInline.forEach(function (treeContainerElement) { + const treeContainer = $(treeContainerElement); + const treeJs = treeContainer.find('.entity-reference-tree'); + const fieldEditName = treeContainer.find(".entity-reference-tree-widget-field").val(); + const widgetElement = $("#" + fieldEditName); + const theme = treeJs.attr("theme"); + const dots = treeJs.attr("dots"); + + // Move the inline widget above the description. Claro uses '.form-item__description'. Seven uses '.description'. + const descriptionElement = treeContainer.closest('.field--type-entity-reference').find('.form-item__description, .description'); + if (descriptionElement.length) { + treeContainer.insertBefore(descriptionElement); + } + + // Create selected text container if missing. + if (!treeContainer.find(".entity-reference-tree-selected-text").length) { + treeContainer.append('<div class="entity-reference-tree-selected-text"></div>'); + } + + // Avoid ajax callback from running following codes again. + if (widgetElement.length) { + const entityType = treeContainer.find(".entity-reference-tree-entity-type").val(); + const bundle = treeContainer.find(".entity-reference-tree-entity-bundle").val(); + const token = settings["entity_tree_token_" + fieldEditName]; + const idIsString = bundle === "*"; + const limit = parseInt(settings["tree_limit_" + fieldEditName]); + let selectedNodes; + + // Selected nodes. + if (idIsString) { + selectedNodes = widgetElement.val().match(/\([a-z 0-9 _]+\)/g); + } else { + selectedNodes = widgetElement.val().match(/\((\d+)\)/g); + } + + // Calculate remaining selected entities. + let remaining; + if (limit > 0) { + remaining = limit + " " + Drupal.t("max"); + } else { + remaining = Drupal.t("unlimited"); + } + + if (selectedNodes) { + // Pick up nodes id. + for (let i = 0; i < selectedNodes.length; i++) { + // Remove the round brackets. + if (idIsString) { + selectedNodes[i] = selectedNodes[i].slice( + 1, + selectedNodes[i].length - 1 + ); + } else { + selectedNodes[i] = parseInt( + selectedNodes[i].slice(1, selectedNodes[i].length - 1), + 10 + ); + } + } + } else { + selectedNodes = []; + } + + treeContainer.find(".entity-reference-tree-selected-node").val(widgetElement.val()); + // Initial selected-entities text. + treeContainer.find(".entity-reference-tree-selected-text").text( + Drupal.t("Selected") + " (0 " + Drupal.t("of") + " " + remaining + "): " + widgetElement.val() + ); + + // Build the tree. + treeJs.jstree({ + core: { + data: { + url: function () { + return Drupal.url( + "admin/entity_reference_tree/json/" + + entityType + + "/" + + bundle + + "?token=" + + token + ); + }, + data: function (node) { + return { + id: node.id, + text: node.text, + parent: node.parent + }; + } + }, + themes: { + dots: dots === "1", + name: theme + }, + multiple: limit !== 1 + }, + checkbox: { + three_state: false + }, + search: { + show_only_matches: true + }, + conditionalselect: function (node) { + // A bundle node can't be selected. + if (node.data && node.data.isBundle) { + return false; + } + if (limit > 1) { + return this.get_selected().length < limit || node.state.selected; + } else { + // No limit. + return true; + } + }, + plugins: ["search", "changed", "checkbox", "conditionalselect"] + }); + + // Initialize the selected node. + treeJs.on("ready.jstree", function (e, data) { + data.instance.select_node(selectedNodes); + }); + + // Selected event. + treeJs.on("changed.jstree", function (evt, data) { + // selected node objects. + const choosedNodes = data.selected; + const r = []; + + for (let i = 0; i < choosedNodes.length; i++) { + const node = data.instance.get_node(choosedNodes[i]); + // node text escaping double quote. + let nodeText = + node.text.replace(/"/g, '""') + " (" + node.id + ")"; + // Comma is a special character for autocomplete widget. + if ( + nodeText.indexOf(",") !== -1 || + nodeText.indexOf("'") !== -1 + ) { + nodeText = '"' + nodeText + '"'; + } + r.push(nodeText); + } + + const selectedText = r.join(", "); + widgetElement.val(selectedText); + // Selected-entities text. + treeContainer.find(".entity-reference-tree-selected-text").text( + Drupal.t("Selected") + " (" + choosedNodes.length + " " + Drupal.t("of") + " " + remaining + "): " + selectedText + ); + }); + + // Search filter box. + let to = false; + treeContainer.find(".entity-reference-tree-search").keyup(function () { + const searchInput = $(this); + if (to) { + clearTimeout(to); + } + to = setTimeout(function () { + const v = searchInput.val(); + treeJs.jstree(true).search(v); + }, 250); + }); + } + }); + } + }; +})(jQuery, Drupal, once); diff --git a/src/Plugin/Field/FieldWidget/InlineEntityReferenceTreeWidget.php b/src/Plugin/Field/FieldWidget/InlineEntityReferenceTreeWidget.php new file mode 100644 index 0000000..86db584 --- /dev/null +++ b/src/Plugin/Field/FieldWidget/InlineEntityReferenceTreeWidget.php @@ -0,0 +1,203 @@ +<?php + +namespace Drupal\entity_reference_tree\Plugin\Field\FieldWidget; + +use Drupal\Component\Utility\Xss; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Field\FieldItemListInterface; + +/** + * Plugin implementation of the 'inline_entity_reference_tree_widget' widget. + * + * @FieldWidget( + * id = "inline_entity_reference_tree_widget", + * label = @Translation("Entity reference tree widget - Inline"), + * field_types = { + * "entity_reference", + * }, + * multiple_values = TRUE + * ) + */ +class InlineEntityReferenceTreeWidget extends EntityReferenceTreeWidget { + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + + $element = parent::formElement($items, $delta, $element, $form, $form_state); + + $theme = $this->getSetting('theme'); + $dots = $this->getSetting('dots'); + $edit_id = $element['target_id']['#id']; + $entity_type = $element['target_id']['#target_type']; + $bundles = $element['target_id']['#selection_settings']['target_bundles']; + $bundles_string = empty($bundles) ? '*' : implode(',', $bundles); + + // Attach libraries with dependencies. + $element['#attached']['library'][] = 'entity_reference_tree/entity_tree_inline'; + $form['#attached']['library'][] = 'entity_reference_tree/jstree_' . $theme . '_theme'; + + // Instance a entity tree builder for this entity type if it exists. + if (\Drupal::hasService('entity_reference_' . $entity_type . '_tree_builder')) { + $treeBuilder = \Drupal::service('entity_reference_' . $entity_type . '_tree_builder'); + } + else { + $treeBuilder = \Drupal::service('entity_reference_entity_tree_builder'); + } + + $entityTrees = []; + + foreach ($bundles as $bundle_id) { + $tree = $treeBuilder->loadTree($entity_type, $bundle_id); + if (!empty($tree)) { + foreach ($tree as $entity) { + // Create tree node for each entity. + // Store them into an array passed to JS. + // An array in JavaScript is indexed list. + // JavaScript's array indices are always sequential + // and start from 0. + $treeNode = $treeBuilder->createTreeNode($entity); + // Applies a very permissive XSS/HTML filter for node text. + $treeNode['text'] = Xss::filterAdmin($treeNode['text']); + $entityTrees[] = $treeNode; + } + } + } + + // Pass data to js file. + $element['#attached']['drupalSettings'] = [ + 'entity_tree_token_' . $edit_id => \Drupal::csrfToken()->get($bundles_string), + 'tree_limit_' . $edit_id => $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(), + 'widget_type' => 'inline', + ]; + + // All around wrapper. + $element['tree_container'] = [ + '#type' => 'html_tag', + '#tag' => 'div', + '#attributes' => [ + 'class' => [ + 'inline-entity-reference-tree-wrapper', + ], + ], + ]; + + // Search filter box. + $element['tree_container']['tree_search'] = [ + '#type' => 'textfield', + '#title' => $this + ->t('Search'), + '#size' => 60, + '#attributes' => [ + 'class' => [ + 'entity-reference-tree-search', + ], + ], + ]; + + // Field element id. + $element['tree_container']['field_id'] = [ + '#name' => 'field_id', + '#type' => 'hidden', + '#weight' => 80, + '#value' => $edit_id, + '#attributes' => [ + 'class' => [ + 'entity-reference-tree-widget-field', + ], + ], + ]; + + // Entity type. + $element['tree_container']['entity_type'] = [ + '#name' => 'entity_type', + '#type' => 'hidden', + '#weight' => 80, + '#value' => $entity_type, + '#attributes' => [ + 'class' => [ + 'entity-reference-tree-entity-type', + ], + ], + ]; + + // Entity bundle. + $element['tree_container']['entity_bundle'] = [ + '#name' => 'entity_bundle', + '#type' => 'hidden', + '#weight' => 80, + '#value' => $bundles, + '#attributes' => [ + 'class' => [ + 'entity-reference-tree-entity-bundle', + ], + ], + ]; + + // JsTree container. + $element['tree_container']['js_tree'] = [ + '#type' => 'html_tag', + '#tag' => 'div', + '#attributes' => [ + 'class' => [ + 'entity-reference-tree', + ], + 'theme' => $theme, + 'dots' => $dots, + ], + ]; + + // Remove dialog link from parent. + unset($element['dialog_link']); + + // Hide autocomplete element. + $element['target_id']['#attributes']['class'][] = 'hidden'; + + return $element; + + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $element = []; + // JsTRee theme. + $element['theme'] = [ + '#type' => 'radios', + '#title' => $this->t('JsTree theme'), + '#default_value' => $this->getSetting('theme'), + '#required' => TRUE, + '#options' => [ + 'default' => $this->t('Default'), + 'default-dark' => $this->t('Default Dark'), + ], + ]; + // Tree dot. + $element['dots'] = [ + '#type' => 'radios', + '#title' => $this->t('Dot line'), + '#default_value' => $this->getSetting('dots'), + '#options' => [ + 0 => $this->t('No'), + 1 => $this->t('Yes'), + ], + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = []; + // JsTree theme. + $summary[] = $this->t('JsTree theme: @theme', ['@theme' => $this->getSetting('theme')]); + $summary[] = $this->t('JsTree dots: @dots', ['@dots' => $this->getSetting('dots') ? $this->t('Yes') : $this->t('No')]); + + return $summary; + } + +} -- GitLab