Heslo s okem

Změna kódu v produkci

Oko se zapíná v definici formuláře přes ->setOption('eye', true). Latte se na volbu doptá, vykreslí vedle inputu button a na input ho naváže přes data atributy. Chování obstará jeden nový skript. Příklad je na reálném poli — heslo FTP v migraci webu.

1. Definice formuláře — zapnout oko

vas-hosting/app/Module/Admin/Domain/Software/Form/MigrateDomainFormFactory.php

Oko je volba na prvku, stejně jako dnešní setOption('generator', true). Bez ní se nic nemění — všechna ostatní pole hesla zůstávají, jak jsou.

-		$form->addPassword('ftpPassword', $this->translator->translate('WS_F_PASSWORD'));
+		$form->addPassword('ftpPassword', $this->translator->translate('WS_F_PASSWORD'))
+			->setOption('eye', true);

2. Sdílený blok v latte — doptat se na oko

vas-hosting/app/Module/Admin/@Templates/@Form/password.latte

Nový define vedle stávajícího passwordInput (generátor). Soubor se importuje v @Layout/layout.latte, modal.latte i auth.latte, takže blok je dostupný všude bez dalšího importu. Na getOption('eye') se ptá blok, ne každá šablona — bez volby vykreslí obyčejný input.

{define passwordEyeInput}
    {varType string $inputName}
    {var $input = $form[$inputName]}

    {if $input->getOption('eye') === true}
        <div class="input-group">
            <input n:name="$inputName" class="form-control">
            <button type="button" class="input-group-text _password-toggle"
                    data-for="{$input->getHtmlId()}"
                    data-label-show="{_'Zobrazit heslo'}"
                    data-label-hide="{_'Skrýt heslo'}"
                    aria-controls="{$input->getHtmlId()}"
                    aria-pressed="false"
                    aria-label="{_'Zobrazit heslo'}"
                    title="{_'Zobrazit heslo'}">
                <i class="bi-eye" aria-hidden="true"></i>
            </button>
        </div>
    {else}
        <input n:name="$inputName" class="form-control">
    {/if}
{/define}

3. Šablona formuláře — použít blok

vas-hosting/app/Module/Admin/@Templates/Domain/Software/Modal/migrate.latte

Ručně psaný {input} se nahradí includem, stejně jako se dnes volá {include passwordInput …}. type="password" už se nepřepisuje — addPassword() ho dá sám.

                     <div class="mb-3 position-relative">
                         {label ftpPassword class => 'form-label required' /}
-                        {input ftpPassword class => 'form-control', type => 'password'}
+                        {include passwordEyeInput inputName => 'ftpPassword', form => $form}
                     </div>

4. Co z toho vyleze

výsledné HTML

Button se na input váže přes data-for = HTML id inputu. Přeložené popisky jdou v data-label-show / data-label-hide, JS v sobě žádné texty nemá.

<div class="mb-3 position-relative">
    <label for="frm-migrateForm-ftpPassword" class="form-label required">Heslo</label>
    <div class="input-group">
        <input type="password" name="ftpPassword" id="frm-migrateForm-ftpPassword" class="form-control">
        <button type="button" class="input-group-text _password-toggle"
                data-for="frm-migrateForm-ftpPassword"
                data-label-show="Zobrazit heslo"
                data-label-hide="Skrýt heslo"
                aria-controls="frm-migrateForm-ftpPassword"
                aria-pressed="false"
                aria-label="Zobrazit heslo"
                title="Zobrazit heslo">
            <i class="bi-eye" aria-hidden="true"></i>
        </button>
    </div>
</div>

5. Chování — nový skript

vas-hosting-assets/js/custom/_passwordToggle.js

Ve stylu _passwords.js: třídy s podtržítkem, init na DOMContentLoaded i DOMContentRefreshed (ajaxové snippety a modaly).

const elements = [];

const setVisible = (button, input, visible) => {
	const label = visible ? button.dataset.labelHide : button.dataset.labelShow;
	input.type = visible ? 'text' : 'password';
	button.setAttribute('aria-pressed', String(visible));
	button.setAttribute('aria-label', label);
	button.title = label;
	button.querySelector('i').className = visible ? 'bi-eye-slash' : 'bi-eye';
};

const init = () => {
	elements.push(...[].slice.call(document.querySelectorAll('._password-toggle')).filter((element) => { return !elements.includes(element) }).map(function (button) {
		const input = document.getElementById(button.dataset.for);
		if (input === null) {
			return button;
		}

		// Nebrat fokus z inputu, kurzor zůstane, kde byl.
		button.addEventListener('mousedown', (e) => e.preventDefault());

		button.addEventListener('click', _ => {
			const start = input.selectionStart;
			const end = input.selectionEnd;
			setVisible(button, input, input.type === 'password');
			input.focus();
			if (start !== null) {
				input.setSelectionRange(start, end);
			}
		});

		// Před odesláním vrátit na password, ať si prohlížeč heslo neuloží jako text.
		if (input.form !== null) {
			input.form.addEventListener('submit', _ => setVisible(button, input, false));
		}

		return button;
	}));
};

document.addEventListener('DOMContentLoaded', init);
document.addEventListener('DOMContentRefreshed', init);

6. Zapojit skript do bundlů

vas-hosting-assets/js/center.js + vas-hosting-assets/js/vpsc.js

Hned za _passwords, v obou bundlech.

 import './custom/_alerts'
 import './custom/_passwords'
+import './custom/_passwordToggle'
 import './custom/_db-name-generator'

7. Volitelně: formuláře vykreslené rendererem

vas-hosting-ui/Form/Renderer/BootstrapRenderer.php → generateControl()

Kroky 2–3 pokrývají ručně psané šablony. Formuláře, které se kreslí celé přes {control form} (např. přihlášení — Auth/Form/LoginFormFactory.php), jdou rendererem a latte blok je nemine. Aby tam setOption('eye', true) fungovalo taky, stačí stejný markup postavit v rendereru, těsně před // App: addition. Chybová hláška se tu vykresluje až za .input-group — chybový stav je potřeba v rendereru ověřit.

		// App: heslo s okem
		if ($control instanceof Controls\TextInput && $control->getOption('eye') === true) {
			$id = $control->getHtmlId();
			$show = $this->translator->translate('Zobrazit heslo');

			$button = Html::el('button', [
				'type' => 'button',
				'class' => 'input-group-text _password-toggle',
				'data-for' => $id,
				'data-label-show' => $show,
				'data-label-hide' => $this->translator->translate('Skrýt heslo'),
				'aria-controls' => $id,
				'aria-pressed' => 'false',
				'aria-label' => $show,
				'title' => $show,
			])->addHtml(Html::el('i', ['class' => 'bi-eye', 'aria-hidden' => 'true']));

			$els = [Html::el('div', ['class' => 'input-group'])
				->addHtml(implode('', $els))
				->addHtml($button)];
		}

		// App: addition
		$inputWrapper = $this->getWrapper('input container');