Use tomselect for StructuralEntityType

This commit is contained in:
Jan Böhmer 2023-01-29 18:52:24 +01:00
parent f085402cba
commit 8d5427a1c3
6 changed files with 119 additions and 73 deletions

View file

@ -0,0 +1,87 @@
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "tom-select/dist/css/tom-select.bootstrap5.css";
import '../../css/components/tom-select_extensions.css';
import TomSelect from "tom-select";
import {Controller} from "@hotwired/stimulus";
export default class extends Controller {
_tomSelect;
connect() {
let settings = {
allowEmptyOption: true,
selectOnTab: true,
searchField: [
{field: "text", weight : 2},
{field: "parent", weight : 0.5},
],
render: {
item: this.renderItem,
option: this.renderOption,
}
};
this._tomSelect = new TomSelect(this.element, settings);
}
renderItem(data, escape) {
let name = "";
if (data.parent) {
name += escape(data.parent) + "&nbsp;→&nbsp;";
}
name += "<b>" + escape(data.text) + "</b>";
return '<div>' + (data.image ? "<img style='max-height: 1.5rem; max-width: 2rem; margin-right: 5px;' ' src='" + data.image + "'/>" : "") + name + '</div>';
}
renderOption(data, escape) {
//Render empty option as full row
if (data.value === "") {
return '<div>&nbsp;</div>';
}
//Indent the option according to the level
const level_html = '&nbsp;&nbsp;&nbsp;'.repeat(data.level);
let filter_badge = "";
if (data.filetype_filter) {
filter_badge = '<span class="badge bg-warning float-end"><i class="fa-solid fa-file-circle-exclamation"></i>&nbsp;' + escape(data.filetype_filter) + '</span>';
}
let parent_badge = "";
if (data.parent) {
parent_badge = '<span class="ms-3 badge rounded-pill bg-secondary float-end picker-us"><i class="fa-solid fa-folder-tree"></i>&nbsp;' + escape(data.parent) + '</span>';
}
let image = "";
if (data.image) {
image = '<img style="max-height: 1.5rem; max-width: 2rem; margin-left: 5px;" src="' + data.image + '"/>';
}
return '<div>' + level_html + escape(data.text) + image + parent_badge + filter_badge + '</div>';
}
}

View file

@ -60,11 +60,6 @@ class PartLotType extends AbstractType
'label' => 'part_lot.edit.location', 'label' => 'part_lot.edit.location',
'required' => false, 'required' => false,
'disable_not_selectable' => true, 'disable_not_selectable' => true,
'attr' => [
'data-controller' => 'elements--selectpicker',
'title' => 'selectpicker.nothing_selected',
'data-live-search' => true,
],
]); ]);
$builder->add('amount', SIUnitType::class, [ $builder->add('amount', SIUnitType::class, [

View file

@ -25,6 +25,7 @@ namespace App\Form\Type;
use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentType;
use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\AbstractStructuralDBElement;
use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Currency;
use App\Services\Attachments\AttachmentURLGenerator;
use App\Services\Trees\NodesListBuilder; use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use RuntimeException; use RuntimeException;
@ -39,9 +40,9 @@ class CurrencyEntityType extends StructuralEntityType
{ {
protected ?string $base_currency; protected ?string $base_currency;
public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, ?string $base_currency) public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, AttachmentURLGenerator $attachmentURLGenerator, ?string $base_currency)
{ {
parent::__construct($em, $builder); parent::__construct($em, $builder, $attachmentURLGenerator);
$this->base_currency = $base_currency; $this->base_currency = $base_currency;
} }

View file

@ -24,6 +24,7 @@ namespace App\Form\Type;
use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentType;
use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\AbstractStructuralDBElement;
use App\Services\Attachments\AttachmentURLGenerator;
use App\Services\Trees\NodesListBuilder; use App\Services\Trees\NodesListBuilder;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
@ -44,16 +45,18 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class StructuralEntityType extends AbstractType class StructuralEntityType extends AbstractType
{ {
protected EntityManagerInterface $em; protected EntityManagerInterface $em;
protected AttachmentURLGenerator $attachmentURLGenerator;
/** /**
* @var NodesListBuilder * @var NodesListBuilder
*/ */
protected $builder; protected $builder;
public function __construct(EntityManagerInterface $em, NodesListBuilder $builder) public function __construct(EntityManagerInterface $em, NodesListBuilder $builder, AttachmentURLGenerator $attachmentURLGenerator)
{ {
$this->em = $em; $this->em = $em;
$this->builder = $builder; $this->builder = $builder;
$this->attachmentURLGenerator = $attachmentURLGenerator;
} }
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
@ -94,17 +97,14 @@ class StructuralEntityType extends AbstractType
$resolver->setDefault('empty_message', null); $resolver->setDefault('empty_message', null);
$resolver->setDefault('controller', 'elements--selectpicker'); $resolver->setDefault('controller', 'elements--structural-entity-select');
$resolver->setDefault('attr', static function (Options $options) { $resolver->setDefault('attr', static function (Options $options) {
$tmp = [ $tmp = [
'data-controller' => $options['controller'], 'data-controller' => $options['controller'],
'data-live-search' => true,
'title' => 'selectpicker.nothing_selected',
]; ];
if ($options['empty_message']) { if ($options['empty_message']) {
$tmp['data-none-Selected-Text'] = $options['empty_message']; $tmp['data-empty_message'] = $options['empty_message'];
$tmp['title'] = $options['empty_message'];
} }
return $tmp; return $tmp;
@ -119,15 +119,6 @@ class StructuralEntityType extends AbstractType
return $this->builder->typeToNodesList($options['class'], null); return $this->builder->typeToNodesList($options['class'], null);
} }
public function buildView(FormView $view, FormInterface $form, array $options): void
{
//Allow HTML in labels. You must override the 'choice_widget_options' block, so that can work
//See extendedBootstrap4_layout.html.twig for that...
$view->vars['use_html_in_labels'] = true;
parent::buildView($view, $form, $options);
}
public function getParent(): string public function getParent(): string
{ {
return ChoiceType::class; return ChoiceType::class;
@ -221,35 +212,10 @@ class StructuralEntityType extends AbstractType
return $this->em->find($options['class'], $value->getID()); return $this->em->find($options['class'], $value->getID());
} }
/**
* This generates the HTML code that will be rendered by selectpicker
* @return string
*/
protected function getChoiceContent(AbstractStructuralDBElement $choice, $key, $value, $options): string
{
$html = "";
//Add element name, use a class as whitespace which hides when not used in dropdown list
$html .= $this->getElementNameWithLevelWhitespace($choice, $options, '<span class="picker-level"></span>');
if ($options['show_fullpath_in_subtext'] && null !== $choice->getParent()) {
$html .= '<span class="ms-3 badge rounded-pill bg-secondary float-end picker-us"><i class="fa-solid fa-folder-tree"></i>&nbsp;' . trim(htmlspecialchars($choice->getParent()->getFullPath())) . '</span>';
}
if ($choice instanceof AttachmentType && !empty($choice->getFiletypeFilter())) {
$html .= '<span class="ms-3 badge bg-warning"><i class="fa-solid fa-file-circle-exclamation"></i>&nbsp;' . trim(htmlspecialchars($choice->getFiletypeFilter())) . '</span>';
}
return $html;
}
protected function generateChoiceAttr(AbstractStructuralDBElement $choice, $key, $value, $options): array protected function generateChoiceAttr(AbstractStructuralDBElement $choice, $key, $value, $options): array
{ {
$tmp = []; $tmp = [];
//Disable attribute if the choice is marked as not selectable //Disable attribute if the choice is marked as not selectable
if ($options['disable_not_selectable'] && $choice->isNotSelectable()) { if ($options['disable_not_selectable'] && $choice->isNotSelectable()) {
$tmp += ['disabled' => 'disabled']; $tmp += ['disabled' => 'disabled'];
@ -259,8 +225,22 @@ class StructuralEntityType extends AbstractType
$tmp += ['data-filetype_filter' => $choice->getFiletypeFilter()]; $tmp += ['data-filetype_filter' => $choice->getFiletypeFilter()];
} }
//Add the HTML content that will be shown finally in the selectpicker $level = $choice->getLevel();
$tmp += ['data-content' => $this->getChoiceContent($choice, $key, $value, $options)]; /** @var AbstractStructuralDBElement|null $parent */
$parent = $options['subentities_of'];
if (null !== $parent) {
$level -= $parent->getLevel() - 1;
}
$tmp += [
'data-level' => $level,
'data-parent' => $choice->getParent() ? $choice->getParent()->getFullPath() : null,
'data-image' => $choice->getMasterPictureAttachment() ? $this->attachmentURLGenerator->getThumbnailURL($choice->getMasterPictureAttachment(), 'thumbnail_xs') : null,
];
if ($choice instanceof AttachmentType && !empty($choice->getFiletypeFilter())) {
$tmp += ['data-filetype_filter' => $choice->getFiletypeFilter()];
}
return $tmp; return $tmp;
} }
@ -285,7 +265,6 @@ class StructuralEntityType extends AbstractType
protected function generateChoiceLabels(AbstractStructuralDBElement $choice, $key, $value, $options): string protected function generateChoiceLabels(AbstractStructuralDBElement $choice, $key, $value, $options): string
{ {
//Just for compatibility reasons for the case selectpicker should not work. The real value is generated in the getChoiceContent() method return $choice->getName();
return $this->getElementNameWithLevelWhitespace($choice, $options, " ");
} }
} }

View file

@ -101,13 +101,13 @@ class AttachmentURLGenerator
/** /**
* Returns a URL under which the attachment file can be viewed. * Returns a URL under which the attachment file can be viewed.
* @return string|null The URL or null if the attachment file is not existing
*/ */
public function getViewURL(Attachment $attachment): string public function getViewURL(Attachment $attachment): ?string
{ {
$absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment); $absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment);
if (null === $absolute_path) { if (null === $absolute_path) {
throw new RuntimeException('The given attachment is external or has no valid file, so no URL can get generated for it! return null;
Use Attachment::getURL() to get the external URL!');
} }
$asset_path = $this->absolutePathToAssetPath($absolute_path); $asset_path = $this->absolutePathToAssetPath($absolute_path);
@ -122,8 +122,9 @@ class AttachmentURLGenerator
/** /**
* Returns a URL to an thumbnail of the attachment file. * Returns a URL to an thumbnail of the attachment file.
* @return string|null The URL or null if the attachment file is not existing
*/ */
public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): string public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string
{ {
if (!$attachment->isPicture()) { if (!$attachment->isPicture()) {
throw new InvalidArgumentException('Thumbnail creation only works for picture attachments!'); throw new InvalidArgumentException('Thumbnail creation only works for picture attachments!');
@ -135,7 +136,7 @@ class AttachmentURLGenerator
$absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment); $absolute_path = $this->attachmentHelper->toAbsoluteFilePath($attachment);
if (null === $absolute_path) { if (null === $absolute_path) {
throw new RuntimeException('The given attachment is external or has no valid file, so no URL can get generated for it!'); return null;
} }
$asset_path = $this->absolutePathToAssetPath($absolute_path); $asset_path = $this->absolutePathToAssetPath($absolute_path);

View file

@ -20,23 +20,6 @@
col-sm-9 col-sm-9
{%- endblock form_group_class %} {%- endblock form_group_class %}
{%- block choice_widget_options -%}
{% for group_label, choice in options %}
{%- if choice is iterable -%}
<optgroup label="{{ choice_translation_domain is same as(false) ? group_label : group_label|trans({}, choice_translation_domain) }}">
{% set options = choice %}
{{- block('choice_widget_options') -}}
</optgroup>
{%- else -%}
{% if use_html_in_labels is defined and use_html_in_labels %}
<option value="{{ choice.value }}"{% if choice.attr %}{% with { attr: choice.attr } %}{{ block('attributes') }}{% endwith %}{% endif %}{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice_translation_domain is same as(false) ? choice.label|raw : choice.label|trans({}, choice_translation_domain)|raw }}</option>
{% else %}
<option value="{{ choice.value }}"{% if choice.attr %}{% with { attr: choice.attr } %}{{ block('attributes') }}{% endwith %}{% endif %}{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice_translation_domain is same as(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }}</option>
{% endif %}
{%- endif -%}
{% endfor %}
{%- endblock choice_widget_options -%}
{% block si_unit_widget %} {% block si_unit_widget %}
<div class="input-group {% if sm %}input-group-sm{% endif %}"> <div class="input-group {% if sm %}input-group-sm{% endif %}">
{{ form_widget(form.value) }} {{ form_widget(form.value) }}