diff --git a/assets/controllers.json b/assets/controllers.json
index 480ad64e..29ea244b 100644
--- a/assets/controllers.json
+++ b/assets/controllers.json
@@ -1,14 +1,5 @@
{
"controllers": {
- "@symfony/ux-toggle-password": {
- "toggle-password": {
- "enabled": true,
- "fetch": "eager",
- "autoimport": {
- "@symfony/ux-toggle-password/dist/style.min.css": true
- }
- }
- },
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
diff --git a/assets/controllers/toggle_password_controller.js b/assets/controllers/toggle_password_controller.js
new file mode 100644
index 00000000..bef87e11
--- /dev/null
+++ b/assets/controllers/toggle_password_controller.js
@@ -0,0 +1,86 @@
+/*
+ * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
+ *
+ * Copyright (C) 2019 - 2025 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 .
+ */
+
+import { Controller } from '@hotwired/stimulus';
+import '../css/components/toggle_password.css';
+
+export default class extends Controller {
+ static values = {
+ visibleLabel: { type: String, default: 'Show' },
+ visibleIcon: { type: String, default: 'Default' },
+ hiddenLabel: { type: String, default: 'Hide' },
+ hiddenIcon: { type: String, default: 'Default' },
+ buttonClasses: Array,
+ };
+
+ isDisplayed = false;
+ visibleIcon = ``;
+ hiddenIcon = ``;
+
+ connect() {
+ if (this.visibleIconValue !== 'Default') {
+ this.visibleIcon = this.visibleIconValue;
+ }
+
+ if (this.hiddenIconValue !== 'Default') {
+ this.hiddenIcon = this.hiddenIconValue;
+ }
+
+ const button = this.createButton();
+
+ this.element.insertAdjacentElement('afterend', button);
+ this.dispatchEvent('connect', { element: this.element, button });
+ }
+
+ /**
+ * @returns {HTMLButtonElement}
+ */
+ createButton() {
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.classList.add(...this.buttonClassesValue);
+ button.setAttribute('tabindex', '-1');
+ button.addEventListener('click', this.toggle.bind(this));
+ button.innerHTML = `${this.visibleIcon} ${this.visibleLabelValue}`;
+ return button;
+ }
+
+ /**
+ * Toggle input type between "text" or "password" and update label accordingly
+ */
+ toggle(event) {
+ this.isDisplayed = !this.isDisplayed;
+ const toggleButtonElement = event.currentTarget;
+ toggleButtonElement.innerHTML = this.isDisplayed
+ ? `${this.hiddenIcon} ${this.hiddenLabelValue}`
+ : `${this.visibleIcon} ${this.visibleLabelValue}`;
+ this.element.setAttribute('type', this.isDisplayed ? 'text' : 'password');
+ this.dispatchEvent(this.isDisplayed ? 'show' : 'hide', { element: this.element, button: toggleButtonElement });
+ }
+
+ dispatchEvent(name, payload) {
+ this.dispatch(name, { detail: payload, prefix: 'toggle-password' });
+ }
+}
diff --git a/assets/css/components/toggle_password.css b/assets/css/components/toggle_password.css
new file mode 100644
index 00000000..f1f4a889
--- /dev/null
+++ b/assets/css/components/toggle_password.css
@@ -0,0 +1,41 @@
+/*
+ * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
+ *
+ * Copyright (C) 2019 - 2025 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 .
+ */
+
+.toggle-password-container {
+ position: relative;
+}
+.toggle-password-icon {
+ height: 1rem;
+ width: 1rem;
+}
+.toggle-password-button {
+ align-items: center;
+ background-color: transparent;
+ border: none;
+ column-gap: 0.25rem;
+ display: flex;
+ flex-direction: row;
+ font-size: 0.875rem;
+ justify-items: center;
+ height: 1rem;
+ line-height: 1.25rem;
+ position: absolute;
+ right: 0.5rem;
+ top: -1.25rem;
+}
diff --git a/composer.json b/composer.json
index a602e505..8e3d1194 100644
--- a/composer.json
+++ b/composer.json
@@ -80,7 +80,6 @@
"symfony/string": "7.3.*",
"symfony/translation": "7.3.*",
"symfony/twig-bundle": "7.3.*",
- "symfony/ux-toggle-password": "^2.29",
"symfony/ux-translator": "^2.10",
"symfony/ux-turbo": "^2.0",
"symfony/validator": "7.3.*",
diff --git a/composer.lock b/composer.lock
index 0bb19aa5..6b9888d7 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "3b97b6338827ba56e0404860f3e98359",
+ "content-hash": "09b78f345ea8115b5b29ea3e67dcb579",
"packages": [
{
"name": "amphp/amp",
@@ -15206,90 +15206,6 @@
],
"time": "2025-06-27T19:55:54+00:00"
},
- {
- "name": "symfony/ux-toggle-password",
- "version": "v2.30.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/ux-toggle-password.git",
- "reference": "414b1ea51b93c4c6c6cc3a485adbfc8764ea6dc8"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/ux-toggle-password/zipball/414b1ea51b93c4c6c6cc3a485adbfc8764ea6dc8",
- "reference": "414b1ea51b93c4c6c6cc3a485adbfc8764ea6dc8",
- "shasum": ""
- },
- "require": {
- "php": ">=8.1",
- "symfony/config": "^5.4|^6.0|^7.0|^8.0",
- "symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0",
- "symfony/form": "^5.4|^6.0|^7.0|^8.0",
- "symfony/http-kernel": "^5.4|^6.0|^7.0|^8.0",
- "symfony/options-resolver": "^5.4|^6.0|^7.0|^8.0",
- "symfony/translation": "^5.4|^6.0|^7.0|^8.0"
- },
- "require-dev": {
- "symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0",
- "symfony/phpunit-bridge": "^5.4|^6.0|^7.0|^8.0",
- "symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
- "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0",
- "twig/twig": "^2.14.7|^3.0.4"
- },
- "type": "symfony-bundle",
- "extra": {
- "thanks": {
- "url": "https://github.com/symfony/ux",
- "name": "symfony/ux"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\UX\\TogglePassword\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Félix Eymonot",
- "email": "felix.eymonot@alximy.io"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Toggle visibility of password inputs for Symfony Forms",
- "homepage": "https://symfony.com",
- "keywords": [
- "symfony-ux"
- ],
- "support": {
- "source": "https://github.com/symfony/ux-toggle-password/tree/v2.30.0"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://github.com/nicolas-grekas",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2025-08-27T15:25:48+00:00"
- },
{
"name": "symfony/ux-translator",
"version": "v2.30.0",
diff --git a/config/bundles.php b/config/bundles.php
index 084e6870..ae7dc9cc 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -33,5 +33,4 @@ return [
Jbtronics\SettingsBundle\JbtronicsSettingsBundle::class => ['all' => true],
Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true],
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
- Symfony\UX\TogglePassword\TogglePasswordBundle::class => ['all' => true],
];
diff --git a/package.json b/package.json
index 1080a74c..7a3efaa4 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,6 @@
"@hotwired/turbo": "^8.0.1",
"@popperjs/core": "^2.10.2",
"@symfony/stimulus-bridge": "^4.0.0",
- "@symfony/ux-toggle-password": "file:vendor/symfony/ux-toggle-password/assets",
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets",
"@symfony/webpack-encore": "^5.0.0",
diff --git a/src/Form/Extension/TogglePasswordTypeExtension.php b/src/Form/Extension/TogglePasswordTypeExtension.php
new file mode 100644
index 00000000..463ec546
--- /dev/null
+++ b/src/Form/Extension/TogglePasswordTypeExtension.php
@@ -0,0 +1,121 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace App\Form\Extension;
+
+use Symfony\Component\Form\AbstractTypeExtension;
+use Symfony\Component\Form\Extension\Core\Type\PasswordType;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
+use Symfony\Component\OptionsResolver\Options;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Translation\TranslatableMessage;
+use Symfony\Contracts\Translation\TranslatorInterface;
+
+final class TogglePasswordTypeExtension extends AbstractTypeExtension
+{
+ public function __construct(private readonly ?TranslatorInterface $translator)
+ {
+ }
+
+ public static function getExtendedTypes(): iterable
+ {
+ return [PasswordType::class];
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'toggle' => false,
+ 'hidden_label' => 'Hide',
+ 'visible_label' => 'Show',
+ 'hidden_icon' => 'Default',
+ 'visible_icon' => 'Default',
+ 'button_classes' => ['toggle-password-button'],
+ 'toggle_container_classes' => ['toggle-password-container'],
+ 'toggle_translation_domain' => null,
+ 'use_toggle_form_theme' => true,
+ ]);
+
+ $resolver->setNormalizer(
+ 'toggle_translation_domain',
+ static fn (Options $options, $labelTranslationDomain) => $labelTranslationDomain ?? $options['translation_domain'],
+ );
+
+ $resolver->setAllowedTypes('toggle', ['bool']);
+ $resolver->setAllowedTypes('hidden_label', ['string', TranslatableMessage::class, 'null']);
+ $resolver->setAllowedTypes('visible_label', ['string', TranslatableMessage::class, 'null']);
+ $resolver->setAllowedTypes('hidden_icon', ['string', 'null']);
+ $resolver->setAllowedTypes('visible_icon', ['string', 'null']);
+ $resolver->setAllowedTypes('button_classes', ['string[]']);
+ $resolver->setAllowedTypes('toggle_container_classes', ['string[]']);
+ $resolver->setAllowedTypes('toggle_translation_domain', ['string', 'bool', 'null']);
+ $resolver->setAllowedTypes('use_toggle_form_theme', ['bool']);
+ }
+
+ public function buildView(FormView $view, FormInterface $form, array $options): void
+ {
+ $view->vars['toggle'] = $options['toggle'];
+
+ if (!$options['toggle']) {
+ return;
+ }
+
+ if ($options['use_toggle_form_theme']) {
+ array_splice($view->vars['block_prefixes'], -1, 0, 'toggle_password');
+ }
+
+ $controllerName = 'toggle-password';
+ $view->vars['attr']['data-controller'] = trim(\sprintf('%s %s', $view->vars['attr']['data-controller'] ?? '', $controllerName));
+
+ if (false !== $options['toggle_translation_domain']) {
+ $controllerValues['hidden-label'] = $this->translateLabel($options['hidden_label'], $options['toggle_translation_domain']);
+ $controllerValues['visible-label'] = $this->translateLabel($options['visible_label'], $options['toggle_translation_domain']);
+ } else {
+ $controllerValues['hidden-label'] = $options['hidden_label'];
+ $controllerValues['visible-label'] = $options['visible_label'];
+ }
+
+ $controllerValues['hidden-icon'] = $options['hidden_icon'];
+ $controllerValues['visible-icon'] = $options['visible_icon'];
+ $controllerValues['button-classes'] = json_encode($options['button_classes'], \JSON_THROW_ON_ERROR);
+
+ foreach ($controllerValues as $name => $value) {
+ $view->vars['attr'][\sprintf('data-%s-%s-value', $controllerName, $name)] = $value;
+ }
+
+ $view->vars['toggle_container_classes'] = $options['toggle_container_classes'];
+ }
+
+ private function translateLabel(string|TranslatableMessage|null $label, ?string $translationDomain): ?string
+ {
+ if (null === $this->translator || null === $label) {
+ return $label;
+ }
+
+ if ($label instanceof TranslatableMessage) {
+ return $label->trans($this->translator);
+ }
+
+ return $this->translator->trans($label, domain: $translationDomain);
+ }
+}
diff --git a/symfony.lock b/symfony.lock
index f484d13c..d301c269 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -729,9 +729,6 @@
},
"files": []
},
- "symfony/ux-toggle-password": {
- "version": "v2.29.2"
- },
"symfony/ux-translator": {
"version": "2.9",
"recipe": {
diff --git a/templates/form/extended_bootstrap_layout.html.twig b/templates/form/extended_bootstrap_layout.html.twig
index 811f57ac..75e44a15 100644
--- a/templates/form/extended_bootstrap_layout.html.twig
+++ b/templates/form/extended_bootstrap_layout.html.twig
@@ -1,5 +1,9 @@
{% extends 'bootstrap_5_horizontal_layout.html.twig' %}
+{%- block toggle_password_widget -%}
+
{{ block('password_widget') }}
+{%- endblock toggle_password_widget -%}
+
{# Make form rows smaller #}
{% block form_row -%}
{%- set row_attr = row_attr|merge({"class": "mb-2"}) -%}
@@ -139,4 +143,4 @@
{% else %}
{{- parent() -}}
{% endif %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/yarn.lock b/yarn.lock
index ba6b118a..307692f2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2023,9 +2023,6 @@
loader-utils "^2.0.0 || ^3.0.0"
schema-utils "^3.0.0 || ^4.0.0"
-"@symfony/ux-toggle-password@file:vendor/symfony/ux-toggle-password/assets":
- version "2.29.2"
-
"@symfony/ux-translator@file:vendor/symfony/ux-translator/assets":
version "2.29.2"