Skip to content
Snippets Groups Projects
Commit c5f240db authored by Geoffrey Roberts's avatar Geoffrey Roberts Committed by Mingsong
Browse files

Issue #3059845: Update branch to match latest 2.x-dev and add support for...

Issue #3059845: Update branch to match latest 2.x-dev and add support for inline widget display compatible with Drupal 10
parent 720390c1
No related branches found
No related tags found
1 merge request!13Issue #3059845: Update branch to match latest 2.x-dev and add support for...
Pipeline #397420 passed with warnings
/* 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;
}
......@@ -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
/**
* @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);
<?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;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment