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.
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);
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}
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>
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>
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);
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'
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');