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"