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