Comment: | Modernisation des fiches membres + amélioration UX avec des transitions |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | dev |
Files: | files | file ages | folders |
SHA1: |
2bcf5d3c6a1f71e6b41d07582e37cdd0 |
User & Date: | bohwaz on 2017-05-10 07:02:43 |
Other Links: | branch diff | manifest | tags |
2017-05-11
| ||
05:14 | Garradin a désormais besoin d'un secret unique pour chaque installation check-in: f2346dbb66 user: bohwaz tags: dev | |
2017-05-10
| ||
07:02 | Modernisation des fiches membres + amélioration UX avec des transitions check-in: 2bcf5d3c6a user: bohwaz tags: dev | |
2017-05-09
| ||
07:03 | Implémentation définitive vérification de signature à l'import de fichier, et plutôt que bloquer si le membre n'est plus admin, mettre toutes les catégories en admin. check-in: 623defddc2 user: bohwaz tags: dev | |
Modified src/include/init.php from [4bc86e819b] to [ba8a1afd8b].
︙ | ︙ | |||
113 114 115 116 117 118 119 | if (!defined($const)) { define($const, $value); } } | | | > > | 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | if (!defined($const)) { define($const, $value); } } const WEBSITE = 'http://garradin.eu/'; const PLUGINS_URL = 'https://garradin.eu/plugins/list.json'; const NTP_SERVER = 'fr.pool.ntp.org'; // PHP devrait être assez intelligent pour chopper la TZ système mais nan // il sait pas faire (sauf sur Debian qui a le bon patch pour ça), donc pour // éviter le message d'erreur à la con on définit une timezone par défaut // Pour utiliser une autre timezone, il suffit de définir date.timezone dans // un .htaccess ou dans config.local.php if (!ini_get('date.timezone')) |
︙ | ︙ |
Modified src/include/lib/Garradin/Membres/Champs.php from [a9683b7b29] to [ae54e63bd0].
︙ | ︙ | |||
76 77 78 79 80 81 82 | } return self::$presets; } static public function listUnusedPresets(Champs $champs) { | | | 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | } return self::$presets; } static public function listUnusedPresets(Champs $champs) { return array_diff_key(self::importPresets(), (array) $champs->getAll()); } public function __construct($champs) { if ($champs instanceOf Champs) { $this->champs = $champs->getAll(); |
︙ | ︙ | |||
125 126 127 128 129 130 131 | } if (!property_exists($this->champs, $champ)) return null; if ($key !== null) { | | | 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | } if (!property_exists($this->champs, $champ)) return null; if ($key !== null) { if (property_exists($this->champs->$champ, $key)) return $this->champs->$champ->$key; else return null; } return $this->champs->$champ; } |
︙ | ︙ | |||
275 276 277 278 279 280 281 | if (($config->type == 'multiple' || $config->type == 'select') && empty($config->options)) { throw new UserException('Le champ "'.$name.'" nécessite de comporter au moins une option possible.'); } if (!property_exists($config, 'editable')) { | | | | | 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | if (($config->type == 'multiple' || $config->type == 'select') && empty($config->options)) { throw new UserException('Le champ "'.$name.'" nécessite de comporter au moins une option possible.'); } if (!property_exists($config, 'editable')) { $config->editable = false; } if (!property_exists($config, 'mandatory')) { $config->mandatory = false; } if (!property_exists($config, 'private')) { $config->private = false; } return true; } /** * Ajouter un nouveau champ |
︙ | ︙ | |||
311 312 313 314 315 316 317 | if (!preg_match('!^[a-z][a-z0-9]*(_[a-z0-9]+)*$!', $name)) { throw new UserException('Le nom du champ est invalide : ne sont acceptés que les lettres minuscules et les chiffres (éventuellement séparés par un underscore).'); } $this->_checkField($name, $config); | | | | | | | 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 | if (!preg_match('!^[a-z][a-z0-9]*(_[a-z0-9]+)*$!', $name)) { throw new UserException('Le nom du champ est invalide : ne sont acceptés que les lettres minuscules et les chiffres (éventuellement séparés par un underscore).'); } $this->_checkField($name, $config); $this->champs->$name = $config; return true; } /** * Modifie un champ particulier * @param string $champ Nom du champ * @param string $key Nom de la clé à modifier * @param mixed $value Valeur à affecter * @return boolean true */ public function set($champ, $key, $value) { if (!isset($this->champs->$champ)) { throw new \LogicException('Champ "'.$champ.'" inconnu.'); } // Vérification $config = clone $this->champs->$champ; $config->$key = $value; $this->_checkField($champ, $config); $this->champs->$champ = $config; return true; } /** * Modifie les champs en interne en vérifiant que tout va bien * @param array $champs Liste des champs * @return boolean true |
︙ | ︙ | |||
376 377 378 379 380 381 382 383 384 385 386 387 388 389 | { throw new UserException('Le champ '.$config->get('champ_identifiant') .' est défini comme identifiant à la connexion et ne peut donc être supprimé des fiches membres.'); } foreach ($champs as $name=>&$config) { $this->_checkField($name, $config); } $this->champs = $champs; return true; } | > | 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | { throw new UserException('Le champ '.$config->get('champ_identifiant') .' est défini comme identifiant à la connexion et ne peut donc être supprimé des fiches membres.'); } foreach ($champs as $name=>&$config) { $config = (object) $config; $this->_checkField($name, $config); } $this->champs = $champs; return true; } |
︙ | ︙ | |||
475 476 477 478 479 480 481 | WHERE '.$config->get('champ_identifiant').' = "";'); // Création de l'index unique $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$config->get('champ_identifiant').');'); } // Création des index pour les champs affichés dans la liste des membres | | | 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 | WHERE '.$config->get('champ_identifiant').' = "";'); // Création de l'index unique $db->exec('CREATE UNIQUE INDEX membres_identifiant ON membres ('.$config->get('champ_identifiant').');'); } // Création des index pour les champs affichés dans la liste des membres $listed_fields = array_keys((array) $this->getListedFields()); foreach ($listed_fields as $field) { if ($field === $config->get('champ_identifiant')) { // Il y a déjà un index continue; } |
︙ | ︙ |
Modified src/include/lib/Garradin/Membres/Session.php from [2d466640b5] to [bded1148a6].
︙ | ︙ | |||
174 175 176 177 178 179 180 | return false; } if (!Security_OTP::TOTP($user->secret, $code)) { // Vérifier encore, mais avec le temps NTP // au cas où l'horloge du serveur n'est pas à l'heure | | | 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | return false; } if (!Security_OTP::TOTP($user->secret, $code)) { // Vérifier encore, mais avec le temps NTP // au cas où l'horloge du serveur n'est pas à l'heure $time = Security_OTP::getTimeFromNTP(NTP_SERVER); if (!Security_OTP::TOTP($user->secret, $code, $time)) { return false; } } |
︙ | ︙ | |||
329 330 331 332 333 334 335 | } // On utilise le mot de passe: si l'utilisateur change de mot de passe // toutes les sessions précédentes sont invalidées $hash = hash(self::HASH_ALGO, $row->token . $row->passe); // Vérification du token | | | 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 | } // On utilise le mot de passe: si l'utilisateur change de mot de passe // toutes les sessions précédentes sont invalidées $hash = hash(self::HASH_ALGO, $row->token . $row->passe); // Vérification du token if (!hash_equals($cookie->token, $row->token)) { // Le sélecteur est valide, mais pas le token ? // c'est probablement que le cookie a été volé, qu'un attaquant // a obtenu un nouveau token, et que l'utilisateur se représente // avec un token qui n'est plus valide. // Dans ce cas supprimons toutes les sessions de ce membre pour // le forcer à se re-connecter |
︙ | ︙ |
Modified src/include/lib/Garradin/Template.php from [ad4149b96e] to [bdc7529178].
︙ | ︙ | |||
104 105 106 107 108 109 110 | } function tpl_strftime_fr($ts, $format) { return Utils::strftime_fr($format, $ts); } | | | 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | } function tpl_strftime_fr($ts, $format) { return Utils::strftime_fr($format, $ts); } function tpl_date_fr($ts, $format = 'd/m/Y H:i:s') { return Utils::date_fr($format, $ts); } function tpl_format_droits($params) { $droits = $params['droits']; |
︙ | ︙ |
Modified src/include/lib/Garradin/Utils.php from [9c5200104c] to [51958d7a51].
︙ | ︙ | |||
558 559 560 561 562 563 564 | { return $key . ' = ' . ($value ? 'true' : 'false'); } elseif (is_numeric($value)) { return $key . ' = ' . $value; } | | > | | 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 | { return $key . ' = ' . ($value ? 'true' : 'false'); } elseif (is_numeric($value)) { return $key . ' = ' . $value; } elseif (is_array($value) || is_object($value)) { $out = ''; $value = (array) $value; foreach ($value as $row) { $out .= $get_ini_line($key . '[]', $row) . "\n"; } return substr($out, 0, -1); } else { return $key . ' = "' . str_replace('"', '\\"', $value) . '"'; } }; foreach ($in as $key=>$value) { if ((is_array($value) || is_object($value)) && is_string($key)) { $out .= '[' . $key . "]\n"; foreach ($value as $row_key=>$row_value) { $out .= $get_ini_line($row_key, $row_value) . "\n"; } |
︙ | ︙ |
Modified src/templates/admin/config/index.tpl from [5b3c1849f9] to [c6633c21cb].
︙ | ︙ | |||
18 19 20 21 22 23 24 | <fieldset> <legend>Garradin</legend> <dl> <dt>Version installée</dt> <dd class="help">{$garradin_version} <a href="{$garradin_website}">[Vérifier la disponibilité d'une nouvelle version]</a></dd> <dt>Informations système</dt> | | > > > > > | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <fieldset> <legend>Garradin</legend> <dl> <dt>Version installée</dt> <dd class="help">{$garradin_version} <a href="{$garradin_website}">[Vérifier la disponibilité d'une nouvelle version]</a></dd> <dt>Informations système</dt> <dd class="help"> Version PHP : {$php_version}<br /> Version SQLite : {$sqlite_version}<br /> Heure du serveur : {$server_time|date_fr} ({if $time_diff > -5 && $time_diff < 5}à l'heure{elseif $time_diff < 0}en retard de {$time_diff} secondes{else}en avance de {$time_diff} secondes{/if})<br /> Chiffrement GnuPG : {if $has_gpg_support}disponible, module activé{else}non, module PHP gnupg non installé ?{/if}<br /> </dd> </dl> </fieldset> <fieldset> <legend>Informations sur l'association</legend> <dl> <dt><label for="f_nom_asso">Nom</label> <b title="(Champ obligatoire)">obligatoire</b></dt> |
︙ | ︙ |
Modified src/templates/admin/config/membres.tpl from [894ad69c3d] to [fae223707c].
|
| | > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | {include file="admin/_head.tpl" current="config" js=1} {include file="admin/config/_menu.tpl" current="membres"} {if $error} {if $error == 'OK'} <p class="confirm"> La configuration a bien été enregistrée. </p> {elseif $error == 'ADD_OK'} <p class="confirm"> Le champ a été ajouté à la fin de la liste. </p> {else} <p class="error"> {$error} </p> {/if} {/if} {if $review} <p class="help"> Voici ce à quoi ressemblera la nouvelle fiche de membre, vérifiez vos modifications avant d'enregistrer les changements. </p> <p class="alert"> Attention ! Si vous avez supprimé un champ, les données liées à celui-ci seront supprimées de toutes les fiches de tous les membres. </p> <fieldset> <legend>Fiche membre exemple</legend> <dl> {foreach from=$champs item="champ" key="nom"} {if $nom == 'passe'}{continue}{/if} {html_champ_membre config=$champ name=$nom disabled=true} {if empty($champ.editable) || !empty($champ.private)} |
︙ | ︙ | |||
133 134 135 136 137 138 139 | {if $nom == 'passe'}{continue}{/if} <fieldset id="f_{$nom}"> <legend>{$nom}</legend> <dl> <dt><label>Type</label></dt> <dd>{$champ.type|get_type}</dd> <dt><label for="f_{$nom}_title">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt> | | | | | | | 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | {if $nom == 'passe'}{continue}{/if} <fieldset id="f_{$nom}"> <legend>{$nom}</legend> <dl> <dt><label>Type</label></dt> <dd>{$champ.type|get_type}</dd> <dt><label for="f_{$nom}_title">Titre</label> <b title="(Champ obligatoire)">obligatoire</b></dt> <dd><input type="text" name="champs[{$nom}][title]" id="f_{$nom}_title" value="{form_field data=$champs->$nom name=title}" size="60" required="required" /></dd> <dt><label for="f_{$nom}_help">Aide</label></dt> <dd><input type="text" name="champs[{$nom}][help]" id="f_{$nom}_help" value="{form_field data=$champs->$nom name=help}" size="100" /></dd> <dt><label><input type="checkbox" name="champs[{$nom}][editable]" value="1" {form_field data=$champs->$nom name=editable checked="1"} /> Modifiable par les membres</label></dt> <dd class="help">Si coché, les membres pourront changer cette information depuis leur espace personnel.</dd> <dt><label><input type="checkbox" name="champs[{$nom}][mandatory]" value="1" {form_field data=$champs->$nom name=mandatory checked="1"} /> Champ obligatoire</label></dt> <dd class="help">Si coché, ce champ ne pourra rester vide.</dd> <dt><label><input type="checkbox" name="champs[{$nom}][private]" value="1" {form_field data=$champs->$nom name=private checked="1"} /> Champ privé</label></dt> <dd class="help">Si coché, ce champ ne sera visible et modifiable que par les personnes pouvant gérer les membres, mais pas les membres eux-même.</dd> {if $champ.type == 'select' || $champ.type == 'multiple'} <dt><label>Options disponibles</label></dt> {if $champ.type == 'multiple'} <dd class="help">Attention changer l'ordre des options peut avoir des effets indésirables.</dd> {else} <dd class="help">Attention renommer ou supprimer une option n'affecte pas ce qui a déjà été enregistré dans les fiches des membres.</dd> |
︙ | ︙ | |||
163 164 165 166 167 168 169 | {if $champ.type == 'select' || empty($champ.options) || count($champ.options) < 32} <li><input type="text" name="champs[{$nom}][options][]" value="" size="50" /></li> {/if} </dd> {/if} <dt><label for="f_{$nom}_list_row">Numéro de colonne dans la liste des membres</label></dt> <dd class="help">Laisser vide ou indiquer le chiffre zéro pour que ce champ n'apparaisse pas dans la liste des membres. Inscrire un chiffre entre 1 et 10 pour indiquer l'ordre d'affichage du champ dans le tableau de la liste des membres.</dd> | | | 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | {if $champ.type == 'select' || empty($champ.options) || count($champ.options) < 32} <li><input type="text" name="champs[{$nom}][options][]" value="" size="50" /></li> {/if} </dd> {/if} <dt><label for="f_{$nom}_list_row">Numéro de colonne dans la liste des membres</label></dt> <dd class="help">Laisser vide ou indiquer le chiffre zéro pour que ce champ n'apparaisse pas dans la liste des membres. Inscrire un chiffre entre 1 et 10 pour indiquer l'ordre d'affichage du champ dans le tableau de la liste des membres.</dd> <dd><input type="number" id="f_{$nom}_list_row" name="champs[{$nom}][list_row]" min="0" max="10" value="{form_field data=$champs->$nom name=list_row}" /></dd> </dl> </fieldset> {/foreach} </div> <fieldset id="f_passe"> <legend>Mot de passe</legend> |
︙ | ︙ | |||
205 206 207 208 209 210 211 | } var fields = document.querySelectorAll('#orderFields fieldset'); for (i = 0; i < fields.length; i++) { var field = fields[i]; | | | < < < < | 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | } var fields = document.querySelectorAll('#orderFields fieldset'); for (i = 0; i < fields.length; i++) { var field = fields[i]; field.querySelector('dl').classList.toggle('hidden'); var legend = field.querySelector('legend'); legend.onclick = function () { this.parentNode.querySelector('dl').classList.toggle('hidden'); } legend.className = 'interactive'; legend.title = 'Cliquer pour modifier ce champ'; var actions = document.createElement('div'); actions.className = 'actions'; |
︙ | ︙ | |||
265 266 267 268 269 270 271 | actions.appendChild(down); var edit = document.createElement('a'); edit.className = 'icn edit'; edit.innerHTML = '✎'; edit.title = 'Modifier ce champ'; edit.onclick = function (e) { | | < < < < > > | | 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 | actions.appendChild(down); var edit = document.createElement('a'); edit.className = 'icn edit'; edit.innerHTML = '✎'; edit.title = 'Modifier ce champ'; edit.onclick = function (e) { this.parentNode.parentNode.querySelector('dl').classList.toggle('hidden'); return false; }; actions.appendChild(edit); if (field.id != champ_identifiant && field.id != 'f_passe' && field.id != champ_identite) { var rem = document.createElement('a'); rem.className = 'icn remove'; rem.innerHTML = '✘'; rem.title = 'Enlever ce champ de la fiche'; rem.onclick = function (e) { if (!window.confirm('Êtes-vous sûr de supprimer ce champ des fiches de membre ?')) { return false; } var field = this.parentNode.parentNode; this.parentNode.parentNode.querySelector('dl').classList.add('hidden'); field.classList.toggle('removed'); window.setTimeout(function () { field.parentNode.removeChild(field); }, 1000); return false; }; actions.appendChild(rem); } if (field.querySelector('.options')) { |
︙ | ︙ |
Modified src/www/admin/config/index.php from [d0fb97ed05] to [1aa6f266e6].
︙ | ︙ | |||
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | $error = $e->getMessage(); } } } $tpl->assign('error', $error); $tpl->assign('garradin_version', garradin_version() . ' [' . (garradin_manifest() ?: 'release') . ']'); $tpl->assign('php_version', phpversion()); $v = \SQLite3::version(); $tpl->assign('sqlite_version', $v['versionString']); $tpl->assign('pays', Utils::getCountryList()); $cats = new Membres\Categories; $tpl->assign('membres_cats', $cats->listSimple()); $tpl->assign('champs', $config->get('champs_membres')->getList(true)); $tpl->display('admin/config/index.tpl'); | > > > > > > > | 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | $error = $e->getMessage(); } } } $tpl->assign('error', $error); $server_time = time(); $ntp_time = \KD2\Security_OTP::getTimeFromNTP(NTP_SERVER); $diff = $server_time - $ntp_time; $tpl->assign('garradin_version', garradin_version() . ' [' . (garradin_manifest() ?: 'release') . ']'); $tpl->assign('php_version', phpversion()); $tpl->assign('has_gpg_support', \KD2\Security::canUseEncryption()); $tpl->assign('server_time', $server_time); $tpl->assign('time_diff', $diff); $v = \SQLite3::version(); $tpl->assign('sqlite_version', $v['versionString']); $tpl->assign('pays', Utils::getCountryList()); $cats = new Membres\Categories; $tpl->assign('membres_cats', $cats->listSimple()); $tpl->assign('champs', $config->get('champs_membres')->getList(true)); $tpl->display('admin/config/index.tpl'); |
Modified src/www/admin/config/membres.php from [6f25a6449e] to [a7dd2bb69f].
1 2 3 4 5 6 7 8 9 10 | <?php namespace Garradin; require_once __DIR__ . '/_inc.php'; $error = false; $membres = new Membres; // Restauration de ce qui était en session | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php namespace Garradin; require_once __DIR__ . '/_inc.php'; $error = false; $membres = new Membres; // Restauration de ce qui était en session if ($champs = $session->sessionGet('champs_membres')) { $champs = new Membres\Champs($champs); } else { // Il est nécessaire de créer une nouvelle instance ici, sinon // l'enregistrement des modifs ne marchera pas car les deux instances seront identiques. |
︙ | ︙ | |||
33 34 35 36 37 38 39 | { $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; } else { if (!empty($_POST['reset'])) { | | | | 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | { $error = 'Une erreur est survenue, merci de renvoyer le formulaire.'; } else { if (!empty($_POST['reset'])) { $session->sessionStore('champs_membres', null); Utils::redirect('/admin/config/membres.php'); } elseif (!empty($_POST['review'])) { try { $nouveau_champs = Utils::post('champs'); foreach ($nouveau_champs as $key=>&$cfg) { $cfg['type'] = $champs->get($key, 'type'); } $champs->setAll($nouveau_champs); $session->sessionStore('champs_membres', (string)$champs); Utils::redirect('/admin/config/membres.php?review'); } catch (UserException $e) { $error = $e->getMessage(); } |
︙ | ︙ | |||
94 95 96 97 98 99 100 | { $config['options'] = ['Première option']; } $champs->add($new, $config); } | | | | 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | { $config['options'] = ['Première option']; } $champs->add($new, $config); } $session->sessionStore('champs_membres', (string) $champs); Utils::redirect('/admin/config/membres.php?added'); } catch (UserException $e) { $error = $e->getMessage(); } } elseif (!empty($_POST['save'])) { try { $champs->save(); $session->sessionStore('champs_membres', null); Utils::redirect('/admin/config/membres.php?ok'); } catch (UserException $e) { $error = $e->getMessage(); } } |
︙ | ︙ | |||
134 135 136 137 138 139 140 141 142 | $tpl->register_modifier('get_type', function ($type) use ($types) { return $types[$type]; }); $tpl->assign('csrf_name', Utils::CSRF_field_name('config_membres')); $tpl->assign('csrf_value', Utils::CSRF_create('config_membres')); $tpl->display('admin/config/membres.tpl'); | > > | 134 135 136 137 138 139 140 141 142 143 144 | $tpl->register_modifier('get_type', function ($type) use ($types) { return $types[$type]; }); $tpl->assign('csrf_name', Utils::CSRF_field_name('config_membres')); $tpl->assign('csrf_value', Utils::CSRF_create('config_membres')); $tpl->assign('title', 'Configuration — ' . (isset($_GET['review']) ? 'Confirmer les changements' : 'Fiche membres')); $tpl->display('admin/config/membres.tpl'); |
Modified src/www/admin/static/admin.css from [b2e904a922] to [0a77a33694].
︙ | ︙ | |||
1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 | background: #eee; padding: .5em; } #orderFields fieldset { position: relative; min-height: 2em; } #orderFields fieldset legend { font-size: 1.2em; line-height: .8em; color: #666; } #orderFields fieldset .actions { display: block; position: absolute; | > > > | > > > > > > > > > > > > > > > > > > > > > | 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 | background: #eee; padding: .5em; } #orderFields fieldset { position: relative; min-height: 2em; transition: all 1s; overflow: hidden; max-height: 1000px; } #orderFields fieldset legend { font-size: 1.2em; line-height: .8em; color: #666; } #orderFields fieldset .actions { display: block; position: absolute; top: 1em; right: 1em; } #orderFields fieldset .actions .icn { position: absolute; } #orderFields fieldset dl { overflow: hidden; transition: all .5s; opacity: 1; display: block; max-height: 1000px; } #orderFields fieldset dl.hidden { opacity: 0; max-height: 0; } #orderFields fieldset.removed { max-height: 0; opacity: 0; border-color: red; min-height: 0; height: 0; } #orderFields fieldset .actions .remove { right: 0em; } #orderFields fieldset .actions .edit { right: 1.5em; } #orderFields fieldset .actions .down { right: 3em; } #orderFields fieldset .actions .up { right: 4.5em; } #orderFields fieldset:nth-child(1) .actions .up, #orderFields fieldset:nth-last-child(1) .actions .down { |
︙ | ︙ |