diff --git a/build/api/core/Checker.php b/build/api/core/Checker.php index 108cb3e..47ca7c2 100755 --- a/build/api/core/Checker.php +++ b/build/api/core/Checker.php @@ -124,7 +124,8 @@ // Boolean case 'boolean': - return $checker && ( is_bool($value) || $value === 'false' || $value === 'true' ); + case 'bool': + return $checker && is_bool($value); break; // Objet non vide @@ -138,7 +139,7 @@ break; case 'numeric': - return $checker && (is_numeric($value) || $value == null || $value == 'null'); + return $checker && (is_numeric($value) || $value == null); break; case "float": diff --git a/build/api/core/Request.php b/build/api/core/Request.php index aadc70a..dce1f28 100755 --- a/build/api/core/Request.php +++ b/build/api/core/Request.php @@ -374,6 +374,13 @@ /* (2) Si le paramètre est renseigné (sauf FILE) */ }elseif( $config['typ'] != 'FILE' ){ + // Try to get json representation + $json_rep = json_decode('{"json": '.$this->raw_params[$name].'}', true); + + // Use only if not failed + if( is_array($json_rep) && isset($json_rep['json']) ) + $this->raw_params[$name] = $json_rep['json']; + // Si la verification est fausse, on retourne faux if( !Checker::run($config['typ'], $this->raw_params[$name]) ) return $this->error->set(Err::WrongParam, $rename, $config['typ']); diff --git a/build/api/module/departement/errorsController.php b/build/api/module/departement/errorsController.php new file mode 100644 index 0000000..9cec7e6 --- /dev/null +++ b/build/api/module/departement/errorsController.php @@ -0,0 +1,25 @@ + $repo->getErrors()]; + } + +} \ No newline at end of file diff --git a/build/api/module/excelController.php b/build/api/module/excelController.php index 6f6e1e2..3be39ea 100644 --- a/build/api/module/excelController.php +++ b/build/api/module/excelController.php @@ -301,15 +301,15 @@ class excelController foreach ($allProf as $initials => &$prof){ //create or update the professor category - $catRepo->createOrUpdate($prof["categoryIndex"], utf8_decode($prof["categoryLabel"])); + $catRepo->create($prof["categoryIndex"], $prof["categoryLabel"]); //create the professor, as some names are missing, we replace them by something else if(!$prof["lastName"]) $prof["lastName"] = "missingLastName"; if(!$prof["firstName"]) $prof["firstName"] = "missingFirstName"; - $prof["dbId"] = $profRepo->exists(utf8_decode($prof["lastName"]), utf8_decode($prof["firstName"])); + $prof["dbId"] = $profRepo->exists($prof["lastName"], $prof["firstName"]); if(!$prof["dbId"]){ - $prof["dbId"] = $profRepo->create( utf8_decode($prof["lastName"]), utf8_decode($prof["firstName"]), $prof["categoryIndex"], $prof["hoursToDo"], $initials); + $prof["dbId"] = $profRepo->create( $prof["lastName"], $prof["firstName"], $prof["categoryIndex"], $prof["hoursToDo"], $initials); } } @@ -318,9 +318,9 @@ class excelController $formRepo = Repo::getRepo("formation"); foreach ($allFormations as &$form){ - $form["dbId"] = $formRepo->exists( utf8_decode($form["name"])); + $form["dbId"] = $formRepo->exists( $form["name"]); if(!$form["dbId"]){ - $form["dbId"] = $formRepo->create( utf8_decode($form["name"]), $form["internal"]); + $form["dbId"] = $formRepo->create( $form["name"], $form["internal"]); } } @@ -346,8 +346,8 @@ class excelController $UE["defaultFormationId"] = null; } - $ueRepo->create(utf8_decode($codeUE), - utf8_decode($UE["name"]), + $ueRepo->create($codeUE, + $UE["name"], $UE["required"] == "OBL", $UE["CourseVH"], $UE["TdVH"], @@ -367,19 +367,19 @@ class excelController switch ($type){ case "Course": - $coursRepo->create( utf8_decode($codeUE), + $coursRepo->create( $codeUE, $allProf[$group["professor"]]["dbId"], $group["VH"], $formations); break; case "TD": - $tdRepo->create(utf8_decode($codeUE), + $tdRepo->create($codeUE, $allProf[$group["professor"]]["dbId"], $group["VH"], $formations); break; case "TP": - $tpRepo->create(utf8_decode($codeUE), + $tpRepo->create($codeUE, $allProf[$group["professor"]]["dbId"], $group["VH"], $formations); diff --git a/build/api/module/professor/filterController.php b/build/api/module/professor/filterController.php new file mode 100644 index 0000000..20c5526 --- /dev/null +++ b/build/api/module/professor/filterController.php @@ -0,0 +1,109 @@ + [OPT] Array of formation IDS + * @ues [OPT] Array of UE codes + * + * @return matches Array of matching professor IDs + * + ---------------------------------------------------------*/ + public static function post($args){ + + $formations = null; + $ues = null; + extract($args); + + /** @var ue $ue_repo */ + $ue_repo = Repo::getRepo('ue'); + + + /* (1) If no filter -> return error + ---------------------------------------------------------*/ + /* (1) Exit if no filter */ + if( is_null($formations) && is_null($ues) ) + return ['error' => new Error(Err::MissingParam, 'You must give at least 1 parameter')]; + + /* (2) Init. result array (only keys used for unicity) */ + $matches_uniq = []; + + + + /* (2) Filter by formation + ---------------------------------------------------------*/ + if( !is_null($formations) ){ + + /** @var formation $form_repo */ + $form_repo = Repo::getRepo('formation'); + + /* (1) For each formation -> get request */ + foreach($formations as $form_id){ + + // 1. Ignore if wrong format + if( !is_numeric($form_id) || intval($form_id) !== $form_id ) + continue; + + // 2. Get from repo + $fetched_ids = $form_repo->getProfessors($form_id); + + // 3. Add in unique set + foreach($fetched_ids as $prof_id) + $matches_uniq[ intval($prof_id) ] = null; + + + } + + } + + + + /* (3) Filter by ue + ---------------------------------------------------------*/ + // if( !is_null($ues) ){ + + // /** @var ue $ue_repo */ + // $ue_repo = Repo::getRepo('ue'); + + // /* (1) For each ue -> get request */ + // foreach($ues as $ue_code){ + + // // 1. Ignore if wrong format + // if( !is_numeric($ue_code) || intval($ue_code) !== $ue_code ) + // continue; + + // // 2. Get from repo + // $fetched_ids = $ue_repo->getProfessors($ue_code); + + // // 3. Add in unique set + // foreach($fetched_ids as $prof_id) + // $matches_uniq[ intval($prof_id) ] = null; + + // } + + // } + + + + return ['matches' => array_keys($matches_uniq)]; + + } + +} \ No newline at end of file diff --git a/build/api/module/rootController.php b/build/api/module/rootController.php index 4195916..b210dfb 100644 --- a/build/api/module/rootController.php +++ b/build/api/module/rootController.php @@ -10,7 +10,7 @@ /* Generates the API documentation * */ - public function get($args){ + public function post($args){ extract($args); return [ 'args' => $args ]; diff --git a/build/database/repo/category.php b/build/database/repo/category.php index 9cf8a8d..71fd68f 100644 --- a/build/database/repo/category.php +++ b/build/database/repo/category.php @@ -13,14 +13,15 @@ use database\core\Repo_i; class category extends Repo_i{ - public function createOrUpdate(int $id, string $label){ + public function create(int $id, string $label) : bool{ //create the category or update the label - $st = $this->pdo->prepare("INSERT INTO Categorie (idCategorie, labelCategorie) VALUES (:id, :label) ON DUPLICATE KEY UPDATE labelCategorie = :label;"); + $st = $this->pdo->prepare("INSERT INTO Categorie (idCategorie, labelCategorie) VALUE (:id, :label)"); - $st->execute([ + return $st->execute([ "id" => $id, "label" => $label ]); + } public function delete(int $id) :bool{ diff --git a/build/database/repo/departement.php b/build/database/repo/departement.php new file mode 100644 index 0000000..de9d1e6 --- /dev/null +++ b/build/database/repo/departement.php @@ -0,0 +1,91 @@ +pdo->prepare(" + (SELECT 'NO_FORMATION_ASSIGNED' errorType, 'Cours' entityType, idCours id FROM Cours WHERE idCours NOT IN (SELECT Cours_idCours FROM GroupeCours)) + UNION + (SELECT 'NO_FORMATION_ASSIGNED' errorType, 'TD' entityType, idTD id FROM TD WHERE idTD NOT IN (SELECT TD_idTD FROM GroupeTD)) + UNION + (SELECT 'NO_FORMATION_ASSIGNED' errorType, 'TP' entityType, idTP id FROM TP WHERE idTP NOT IN (SELECT TP_idTP FROM GroupeTP))"); + + $GroupWithoutFormation->execute([]); + + $results = array_merge($results,$GroupWithoutFormation->fetchAll()); + + /* (2) Find Groups without a professor bound */ + + $GroupWithoutProfessor = $this->pdo->prepare(" + (SELECT 'NO_PROFESSOR_ASSIGNED' errorType, 'Cours' entityType, idCours id FROM Cours WHERE Professeur_idProfesseur IS NULL) + UNION + (SELECT 'NO_PROFESSOR_ASSIGNED' errorType, 'TD' entityType, idTD id FROM TD WHERE Professeur_idProfesseur IS NULL) + UNION + (SELECT 'NO_PROFESSOR_ASSIGNED' errorType, 'TP' entityType, idTP id FROM TP WHERE Professeur_idProfesseur IS NULL)"); + + $GroupWithoutProfessor->execute([]); + + $results = array_merge($results,$GroupWithoutProfessor->fetchAll()); + + /* (3) Find Groups a null VH */ + + $GroupWithNullVH = $this->pdo->prepare(" + (SELECT 'NULL_VH' errorType, 'Cours' entityType, idCours id FROM Cours WHERE volume = 0) + UNION + (SELECT 'NULL_VH' errorType, 'TD' entityType, idTD id FROM TD WHERE volume = 0) + UNION + (SELECT 'NULL_VH' errorType, 'TP' entityType, idTP id FROM TP WHERE volume = 0)"); + + $GroupWithNullVH->execute([]); + + $results = array_merge($results,$GroupWithNullVH->fetchAll()); + + /* (4) Find UE with an incorrect VH in the groups compared to what's supposed to be */ + + $UEWithIncorrectGroupVH = $this->pdo->prepare(" + (SELECT 'UNEQUIVALENT_VH' errorType, 'Cours' entityType, U.code + FROM UE U + WHERE + 0 != (SELECT MOD(SUM(volume),U.volumeCours) modCours FROM Cours WHERE UE_code = U.code GROUP BY Cours.UE_code) + ) + + UNION + + (SELECT 'UNEQUIVALENT_VH' errorType, 'TD' entityType, U.code + FROM UE U + WHERE + 0 != (SELECT MOD(SUM(volume),U.volumeTD) modCours FROM TD WHERE UE_code = U.code GROUP BY TD.UE_code) + ) + + UNION + + (SELECT 'UNEQUIVALENT_VH' errorType, 'TP' entityType, U.code + FROM UE U + WHERE + 0 != (SELECT MOD(SUM(volume),U.volumeTP) modCours FROM TP WHERE UE_code = U.code GROUP BY TP.UE_code) + )"); + + $UEWithIncorrectGroupVH->execute([]); + + $results = array_merge($results,$UEWithIncorrectGroupVH->fetchAll()); + + return $results; + } + +} \ No newline at end of file diff --git a/build/database/repo/formation.php b/build/database/repo/formation.php index 6989427..875fda1 100644 --- a/build/database/repo/formation.php +++ b/build/database/repo/formation.php @@ -13,37 +13,84 @@ use database\core\Repo_i; class formation extends Repo_i { - public function create(string $label, bool $isInternal) : int{ - $st = $this->pdo->prepare("INSERT INTO Formation (labelFormation, isInternal) VALUE (:label,:isInternal);"); + /* (1) Creates a new formation + * + * @label The formation label + * @isInternal Whether the formation is internal + * + * @return form_id The formation UID (or NULL on error) + * + ---------------------------------------------------------*/ + public function create(string $label, bool $isInternal) : ?int{ - $st->execute([ - "label" => $label, - "isInternal" => $isInternal? 1 : 0 + /* (1) Prepare statement */ + $st = $this->pdo->prepare("INSERT INTO `Formation` (`labelFormation`, `isInternal`) VALUE (:label, :isInternal);"); + + /* (2) Bind params and execute statement */ + if( is_bool($st) ) return NULL; + $success = $st->execute([ + ':label' => $label, + ':isInternal' => $isInternal ? 1 : 0 ]); + /* (3) Manage error */ + if( !$success ) + return NULL; + + /* (4) Return inserted ID */ return $this->pdo->lastInsertId(); + } - public function exists(string $label) : int{ - $st = $this->pdo->prepare("SELECT idFormation FROM Formation WHERE labelFormation = :label"); - $st->execute([ - "label" => $label - ]); - return $st->fetch()["idFormation"]?: 0; + /* (2) Check if a formation exists (by its label) + * + * @label The formation label + * + * @return form_id The formation UID (or NULL on error) + * + ---------------------------------------------------------*/ + public function exists(string $label) : ?int{ + + /* (1) Prepare statement */ + $st = $this->pdo->prepare("SELECT `idFormation` FROM `Formation` WHERE `labelFormation` = :label"); + + /* (2) Bind params and execute statement */ + if( is_bool($st) ) return NULL; + $success = $st->execute([ ':label' => $label ]); + + /* (3) Manage error */ + if( !$success ) + return NULL; + + /* (4) Return if we have a result */ + + return $st->fetch()['idFormation'] ?: 0; + } + + /* (3) Deletes a formation + * + * @return deleted Whether the formation have been deleeted successfully + * + ---------------------------------------------------------*/ public function delete(int $id) : bool{ - $st = $this->pdo->prepare("DELETE FROM Formation WHERE idFormation = :id"); - return $st->execute([ - "id" => $id - ]); + /* (1) Prepare statement */ + $st = $this->pdo->prepare("DELETE FROM `Formation` WHERE `idFormation` = :id"); + + /* (2) Bind params and execute request */ + if( is_bool($st) ) return false; + + /* (3) Dispatch the execution status */ + return $st->execute([ ':id' => $id ]); + } - /* (x) Gets a formation by its ID || getAll + /* (4) Gets a formation by its ID || getAll * * @form_id [OPT] The formation id, if not set, getAll() * @@ -84,4 +131,44 @@ class formation extends Repo_i { } + + /* (5) Gets all professors who teaches a formation by ids (array) + * + * @form_id The formation id + * + * @return professors The professors' UID matching the @form_id formation + * + ---------------------------------------------------------*/ + public function getProfessors(int $form_id) : array{ + + /* (1) Prepare statement */ + $st = $this->pdo->prepare("SELECT p.idProfesseur + FROM Professeur p, Formation f + WHERE ( + p.idProfesseur IN ( SELECT p_cr.idProfesseur FROM Professeur p_cr, UE ue, Cours c WHERE c.Professeur_idProfesseur = p_cr.idProfesseur AND c.UE_code = ue.code AND ue.Formation_idFormation = f.idFormation ) + OR p.idProfesseur IN ( SELECT p_td.idProfesseur FROM Professeur p_td, UE ue, TD t WHERE t.Professeur_idProfesseur = p_td.idProfesseur AND t.UE_code = ue.code AND ue.Formation_idFormation = f.idFormation ) + OR p.idProfesseur IN ( SELECT p_tp.idProfesseur FROM Professeur p_tp, UE ue, TP t WHERE t.Professeur_idProfesseur = p_tp.idProfesseur AND t.UE_code = ue.code AND ue.Formation_idFormation = f.idFormation ) + ) + AND f.idFormation = :form_id;"); + + /* (2) Bind params and execute statement */ + if( is_bool($st) ) return []; + $success = $st->execute([ ':form_id' => $form_id ]); + + /* (3) Manage error */ + if( !$success ) + return []; + + /* (4) Get data */ + $fetched = $st->fetchAll(); + + /* (5) Return [] on no result */ + if( $fetched === false ) + return []; + + /* (6) Return data */ + return $fetched; + + } + } \ No newline at end of file diff --git a/build/database/repo/professor.php b/build/database/repo/professor.php index 4adb6e0..9a1e73b 100644 --- a/build/database/repo/professor.php +++ b/build/database/repo/professor.php @@ -24,7 +24,7 @@ class professor extends Repo_i { * @isAdmin Whether the professor is an admin * @casLogin The professor's CAS username * - * @return prof_id The professor's UID (or NULL on error) + * @return prof_id The professor's UID (or -1 on error) * ---------------------------------------------------------*/ public function create(string $lastName, string $firstName, int $category, $hoursToDo = 0, $initials = "", $isAdmin = false , $casLogin = "" ) : ?int{ diff --git a/config/modules.json b/config/modules.json index 5b96093..3d6a035 100644 --- a/config/modules.json +++ b/config/modules.json @@ -1,9 +1,21 @@ { - "GET": { + "POST": { "des": "Returns the API documentation", "per": [], "par": { - "URL0": { "des": "Method name", "typ": "varchar(1,30)", "ren": "method_name", "opt": true, "def": null } + "URL0": { "des": "Method name", "typ": "varchar(1,30)", "ren": "method_name", "opt": true, "def": null }, + "mixed": { "des": "mixed type", "typ": "mixed", "opt": true }, + "id": { "des": "id type", "typ": "id", "opt": true }, + "text": { "des": "text type", "typ": "text", "opt": true }, + "mail": { "des": "mail type", "typ": "mail", "opt": true }, + "alphanumeric": { "des": "alphanumeric type", "typ": "alphanumeric", "opt": true }, + "letters": { "des": "letters type", "typ": "letters", "opt": true }, + "array": { "des": "array type", "typ": "array", "opt": true }, + "array_id": { "des": "array type", "typ": "array", "opt": true }, + "boolean": { "des": "boolean type", "typ": "boolean", "opt": true }, + "object": { "des": "object type", "typ": "object", "opt": true }, + "numeric": { "des": "numeric type", "typ": "numeric", "opt": true }, + "float": { "des": "float type", "typ": "float", "opt": true } } }, @@ -59,6 +71,17 @@ } }, + "departement":{ + "errors":{ + "GET": { + "des": "Get the list of incoherence of the department", + "per": [], + "par": { + } + } + } + }, + "professor":{ "POST": { diff --git a/webpack/component/teacher/view.vue b/webpack/component/teacher/view.vue index f2cf5db..fb03530 100644 --- a/webpack/component/teacher/view.vue +++ b/webpack/component/teacher/view.vue @@ -47,7 +47,7 @@
{{ gname }}
- {{ data.name }} + {{ data.name }} diff --git a/webpack/data/common.js b/webpack/data/common.js index be6eb9d..fce6dcd 100644 --- a/webpack/data/common.js +++ b/webpack/data/common.js @@ -1,6 +1,7 @@ import {GlobalStore} from '../lib/gstore' import {APIClient} from '../lib/api-client' import {PopUp} from '../lib/pop-up' +require('../lib/css-class-override') window.gstore = new GlobalStore(); diff --git a/webpack/data/teacher.js b/webpack/data/teacher.js index b5cbdf4..4ee7bbf 100644 --- a/webpack/data/teacher.js +++ b/webpack/data/teacher.js @@ -17,7 +17,73 @@ api.call('GET professor/1/', { vh: true }, function(rs){ -/* (2) Manage Filters +/* (2) Define filters' callback +---------------------------------------------------------*/ +/* (1) Define global callback */ +gstore.add('filter_handler', function(){ + + var request = {}; + + /* (1) Get all filter types */ + var filter_type = Object.keys(gstore.get.filters); + + /* (2) Get all formated filter data */ + for( ftype of filter_type ){ + + // 1. Initialize result filter + request[ftype] = []; + var fl = gstore.get.filters[ftype].length; + + // 2. Add only @code for @active entries + for( var f = 1 ; f < fl ; f++ ){ + + // 2.1. Store filter locally (for easier access) + var filter = gstore.get.filters[ftype][f]; + + // 2.2. If active -> add to list + ( filter.code != null && filter.active ) && request[ftype].push(filter.code); + + } + + } + + + /* (3) Get professor id for matching filter */ + api.call('POST professor/filter', request, function(rs){ + + // 1. Abort on error + if( rs.error !== 0 ) return console.log('No filter result found, error: '+rs.error); + console.log(rs); + + // 2. Fetch professor elements + var local_ptr = gstore.get.professors; + var l = gstore.get.professors.length; + + // 3. For each element + for( var e = 0 ; e < l ; e++ ){ + + // 3.1. Show by default + var element = document.querySelector('section[data-id=\''+local_ptr[e].idProfesseur+'\']'); + if( !element ) continue; + + element.remClass('filter-hidden'); + + // 3.2. Only hide if does not match filter + if( rs.professors.indexOf(local_ptr[e].idProfesseur) <= -1 ) + element.addClass('filter-hidden'); + + } + + + }); + + + +}); + + + +/* (3) Get Filters ---------------------------------------------------------*/ /* (1) Define global filter */ gstore.add('filters', { @@ -35,7 +101,7 @@ api.call('GET formation', {}, function(rs){ // {2} Format UE filters // for( var i = 0 ; i < rs.formations.length ; i++ ) gstore.get.filters.formations.push({ - code: rs.formations[i].id, + code: rs.formations[i].idForm, name: rs.formations[i].labelForm, active: false }); @@ -63,7 +129,7 @@ api.call('GET ue', {}, function(rs){ -/* (3) Manage Instant Search (IS) +/* (4) Manage Instant Search (IS) ---------------------------------------------------------*/ /* (1) Define global timeout index */ gstore.add('is_to', null); @@ -93,19 +159,22 @@ gstore.add('is_handler', function(e){ // 2. For each element for( var e = 0 ; e < l ; e++ ){ - // 2.1. De-activate by default + // 2.1. Show by default var element = document.querySelector('section[data-id=\''+local_ptr[e].idProfesseur+'\']'); if( !element ) continue; - element.className = 'hidden'; + element.remClass('search-hidden'); // 2.2. Extract name components - var fname = local_ptr[e].firstName.trim().toLowerCase(); - var lname = local_ptr[e].lastName.trim().toLowerCase(); + var fname = local_ptr[e].firstName.trim().toLowerCase(); + var lname = local_ptr[e].lastName.trim().toLowerCase(); + var fullname = fname+' '+lname; - // 2.3. Check if matches - if( gstore.get.is_buf.length == 0 || fname.search(gstore.get.is_buf) + lname.search(gstore.get.is_buf) > -2 ) - element.className = ''; + // 2.3. Hide if does not match + var match_offset = gstore.get.is_buf.length == 0 || fname.search(gstore.get.is_buf) + lname.search(gstore.get.is_buf) + fullname.search(gstore.get.is_buf); + + if( match_offset <= -3 ) + element.addClass('search-hidden'); } diff --git a/webpack/lib/css-class-override.js b/webpack/lib/css-class-override.js new file mode 100644 index 0000000..3f99969 --- /dev/null +++ b/webpack/lib/css-class-override.js @@ -0,0 +1,48 @@ +/* Ajout de classe pour un element (surcharge de l'ELEMENT natif) +* +* @className Classe a ajouter a l'element +* +*/ +Element.prototype.addClass = function(className){ + + /* [1] On transforme la classe actuelle en tableau + ====================================================*/ + var classArray = this.className.split(' '); + + /* [2] On ajoute la classe si elle n'y est pas deja + ====================================================*/ + if( classArray.indexOf(className) > -1 ) return; // La classe y est deja + + classArray.push(className); + + /* [3] On reformatte tout + ====================================================*/ + this.className = classArray.join(' ').trim(); + +} + + +/* Suppression de classe pour un element (surcharge de l'ELEMENT natif) +* +* @className Classe a supprimer a l'element +* +*/ +Element.prototype.remClass = function(className){ + + /* [1] On transforme la classe actuelle en tableau + ====================================================*/ + var classArray = this.className.split(' '); + + /* [2] On retire la classe si elle y est + ====================================================*/ + var index = classArray.indexOf(className); + + if( index == -1 ) return; // La classe n'y est pas + + classArray = classArray.slice(0,index).concat( classArray.slice(index+1) ); + + /* [3] On reformatte tout + ====================================================*/ + this.className = classArray.join(' ').trim(); + +} \ No newline at end of file diff --git a/webpack/scss/container.scss b/webpack/scss/container.scss index 84532c9..49a7087 100644 --- a/webpack/scss/container.scss +++ b/webpack/scss/container.scss @@ -150,7 +150,8 @@ color: $primary-color; // hidden mode - &.hidden{ display: none; } + &.search-hidden, + &.filter-hidden{ display: none; } /* (2) Card generic title */ & > span.category{ @@ -431,7 +432,7 @@ display: none; position: relative; - text-indent: 1.5em; + text-indent: 2em; margin-bottom: 1em; cursor: default; @@ -448,6 +449,7 @@ display: block; position: absolute; + margin-left: .5em; width: calc( 1em - 2*2px ); height: calc( 1em - 2*2px );