From ffdb8d4cf7a460d2cd1f5bd8e8cc434205b9ebff Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Thu, 23 Apr 2020 19:53:56 +0200 Subject: [PATCH] LSattr_html :: mail : add autocomplete feature --- .../LSattr_html/LSattr_html_mail.docbook | 126 +++++++++++++- .../css/default/LSformElement_mail.css | 29 ++++ .../class/class.LSformElement_mail.php | 143 +++++++++++++++- public_html/includes/js/LSformElement_mail.js | 25 ++- .../includes/js/LSformElement_mail_field.js | 157 ++++++++++++++++++ 5 files changed, 465 insertions(+), 15 deletions(-) create mode 100644 public_html/css/default/LSformElement_mail.css create mode 100644 public_html/includes/js/LSformElement_mail_field.js diff --git a/doc/conf/LSattribute/LSattr_html/LSattr_html_mail.docbook b/doc/conf/LSattribute/LSattr_html/LSattr_html_mail.docbook index 9806862f..3361bc14 100644 --- a/doc/conf/LSattribute/LSattr_html/LSattr_html_mail.docbook +++ b/doc/conf/LSattribute/LSattr_html/LSattr_html_mail.docbook @@ -1,13 +1,51 @@ LSattr_html_mail Ce type est utilisé pour la gestion des attributs dont la valeur est - une adresse e-mail. Il propose directement dans l'interface, la possibilité - d'envoyer des mails à l'adresse saisie. + une adresse e-mail. Il offre les fonctionnalités suivantes : + + la possibilité d'envoyer des mails directement depuis l'interface + de l'application ; + l'autocomplétion lors de la saisie d'une adresse. + + Structure... array( - 'disableMailSending' => [booléen] + 'disableMailSending' => [booléen], + + // Autocomplétion pour un type d'LSobject donné + 'autocomplete' => array ( + 'object_type' => '[Type d'LSobject]', + 'mail_attributes' => array ( + 'mail', + 'mailAlternateAddress', + [...] + ), + 'filter' => '[filtre LDAP]', + 'basedn' => '[base DN spécifique]', + 'scope' => '[scope de recherche]', + 'displayFormat' => '[LSformat]', + 'onlyAccessible' => [booléen], + ), + + // Autocomplétion sur la base d'une recherche LDAP brute + 'autocomplete' => array ( + 'mail_attributes' => array ( + 'mail', + 'mailAlternateAddress', + [...] + ), + 'filter' => '[filtre LDAP]', + 'basedn' => '[base DN spécifique]', + 'scope' => '[scope de recherche]', + 'displayFormat' => '[LSformat]', + 'onlyAccessible' => [booléen], + ), + + // Autocomplétion (par défaut) + 'autocomplete' => true, + ),]]> ... @@ -25,9 +63,89 @@ + + autocomplete + + Paramètrage de l'autocomplétion des adresses mails saisies : Il peut s'agir + d'un tableau configurant les paramètres de l'autocomplétion ou simplement + true pour activer l'autocomplétion par défaut, c'est à dire la recherche brute + dans l'annuaire de n'importe quel objet ayant l'attribut mail. + En cas de configuration avancée, il est possible de faire une recherche : + + Sur la base d'un type d'&LSobject; donné : l'autocomplétion se fera + alors comme n'importe quelle recherche d'un type d'objet donné. + Sur la base d'une recherche brute dans l'annuaire : l'autocomplétion se + fera alors sur la valeur de l'adresse mail recherchée et au travers une recherche brute dans + l'annuaire sur n'importe quels objets ayant une adresse email correspondant. + + + Les paramètres associés à ces deux cas de figure sont décrits ci-dessous : + + + + object_type + + Le type d'&LSobject; recherché. + + + + + mail_attributes + + Le(s) nom de l'attribut stockant les adresses emails recherchées. Il peut s'agir d'une chaîne + de caractères ou d'un tableau s'il y a plusieurs attributs. + + + + + filter + + Un filtre de recherche falcultatif venant en plus de celui calculé automatiquement à partir + du mot clé de recherche. + + + + + basedn + + Le basedn de la recherche. Paramètre + facultatif. + + + + + scope + + Le scope de la recherche. Paramètre + facultatif, par défaut : sub. + + + + + displayFormat + + Le &LSformat; d'affichage des objets trouvés. Ce paramètre est facultatif et par défaut, + il s'agira du format d'affichage propre au type d'&LSobject; (si défini) et à défaut, l'adresse + mail trouvée sera affichée. + + + + + onlyAccessible + + Booléen falcultatif définissant si seul les &LSobjects; auxquels l'utilisateur connecté à accès + doivent être considérés comme sélectionnables (Faux par défaut). Ce paramètre n'est appliqué que dans + le cas d'une recherche pour un type d'&LSobject; donné. + + + + + + + - Ce type d'attribut HTML est dérivé du type + Ce type d'attribut HTML est dérivé du type text. Il profite donc de toutes les fonctionnalités d'un champ de ce type (autogénération, ...). diff --git a/public_html/css/default/LSformElement_mail.css b/public_html/css/default/LSformElement_mail.css new file mode 100644 index 00000000..fb9c014c --- /dev/null +++ b/public_html/css/default/LSformElement_mail.css @@ -0,0 +1,29 @@ +ul.LSformElement_mail_autocomplete { + border: 1px solid #ccc; + width: 200px; + margin: 0; + margin-top: 0.1em; + max-height: 10em; + overflow: auto; + padding: 0; + list-style-type: none; +} + +li.LSformElement_mail_autocomplete { + cursor: pointer; + border-bottom: 1px dotted #ccc; + font-size: 0.8em; +} + +li.LSformElement_mail_autocomplete:last-of-type { + border: none; +} + +li.LSformElement_mail_autocomplete_over { + background-color: #ccc; +} + +li.LSformElement_mail_autocomplete_current { + font-style: italic; + color: #777; +} diff --git a/public_html/includes/class/class.LSformElement_mail.php b/public_html/includes/class/class.LSformElement_mail.php index c5c14f6f..9faee999 100644 --- a/public_html/includes/class/class.LSformElement_mail.php +++ b/public_html/includes/class/class.LSformElement_mail.php @@ -34,16 +34,21 @@ LSsession :: loadLSclass('LSformElement_text'); class LSformElement_mail extends LSformElement_text { var $JSscripts = array( + 'LSformElement_mail_field.js', 'LSformElement_mail.js' ); - + + var $CSSfiles = array( + 'LSformElement_mail.css', + ); + var $fetchVariables = array( 'uriClass' => 'LSformElement_mail', 'uriPrefix' => 'mailto:' ); - + var $fieldTemplate = 'LSformElement_uri_field.tpl'; - + public function getDisplay() { LSsession :: addHelpInfos ( 'LSformElement_mail', @@ -54,6 +59,9 @@ class LSformElement_mail extends LSformElement_text { if (LSsession :: loadLSclass('LSmail')) { LSmail :: loadDependenciesDisplay(); } + if (!$this -> isFreeze() && $this -> getParam('html_options.autocomplete')) { + LSsession :: addJSconfigParam('LSformElement_mail_autocomplete_noResultLabel', _('No result')); + } return parent :: getDisplay(); } @@ -61,8 +69,135 @@ class LSformElement_mail extends LSformElement_text { if ($this -> getParam('html_options.disableMailSending', false, 'bool')) { $this -> fetchVariables['uriClass'] .= " LSformElement_mail_disableMailSending"; } + if ($this -> getParam('html_options.autocomplete', false, 'bool')) { + $this -> fetchVariables['uriClass'] .= " LSformElement_mail_autocomplete"; + } return parent :: fetchTemplate($template,$variables); } -} + /** + * Autocomplete email + * + * @param[in] $pattern The pattern of the search + * + * @retval array(mail -> displayName) Found emails + */ + public function autocomplete($pattern) { + $ret = array(); + if ($this -> getParam('html_options.autocomplete')) { + $mail_attributes = $this -> getParam('html_options.autocomplete.mail_attributes', array('mail')); + if (!is_array($mail_attributes)) $mail_attributes = array($mail_attributes); + $obj_type = $this -> getParam('html_options.autocomplete.object_type'); + if ($obj_type) { + // Search with a specific objectType + if (LSsession :: loadLSobject($obj_type)) { + $obj = new $obj_type(); + $filters = array(); + foreach($mail_attributes as $attr) { + $filters[] = Net_LDAP2_Filter::create($attr, 'present'); + } + $filter = (count($filters)==1?$filters[0]:Net_LDAP2_Filter::combine('or', $filters)); + if ($this -> getParam('html_options.autocomplete.filter')) { + $filter = Net_LDAP2_Filter::combine( + 'and', + array( + Net_LDAP2_Filter::parse($this -> getParam('html_options.autocomplete.filter')), + $filter, + ) + ); + } + $sparams = array( + 'pattern' => $pattern, + 'attributes' => $mail_attributes, + 'displayFormat' => $this -> getParam('html_options.autocomplete.display_name_format'), + 'filter' => $filter, + 'onlyAccessible' => $this -> getParam('html_options.autocomplete.onlyAccessible', false, 'bool'), + ); + LSdebug($filter->as_string()); + $search = new LSsearch( + $obj_type, + 'LSformElement_mail::autocomplete', + $sparams, + true + ); + $search -> run(); + foreach($search -> getSearchEntries() as $e) { + foreach($mail_attributes as $attr) { + $mails = $e->get($attr); + if (!$mails) continue; + if (!is_array($mails)) $mails = array($mails); + foreach($mails as $mail) + $ret[$mail] = $e->displayName; + } + } + } + } + else { + $filters = array(); + foreach($mail_attributes as $attr) { + $filters[] = Net_LDAP2_Filter::create($attr, 'contains', $pattern); + } + $filter = (count($filters)==1?$filters[0]:Net_LDAP2_Filter::combine('or', $filters)); + if ($this -> getParam('html_options.autocomplete.filter')) { + $filter = Net_LDAP2_Filter::combine( + 'and', + array( + Net_LDAP2_Filter::parse($this -> getParam('html_options.autocomplete.filter')), + $filter, + ) + ); + } + + $displayNameFormat = $this -> getParam('html_options.autocomplete.display_name_format', false); + $attributes = $mail_attributes; + if ($displayNameFormat) + foreach(getFieldInFormat($displayNameFormat) as $attr) + if(!in_array($attr, $attributes)) + $attributes[] = $attr; + + $objects = LSldap :: search ( + $filter, + $this -> getParam('html_options.autocomplete.basedn', null), + array ( + 'attributes' => $attributes, + 'scope' => $this -> getParam('html_options.autocomplete.scope', 'sub'), + ) + ); + + if (is_array($objects)) { + foreach($objects as $object) { + $displayName = ($displayNameFormat?getFData($displayNameFormat, $object['attrs']):null); + foreach($mail_attributes as $attr) { + if (!isset($object['attrs'][$attr])) continue; + $mails = $object['attrs'][$attr]; + if (!is_array($mails)) $mails = array($mails); + foreach($mails as $mail) + $ret[$mail] = ($displayName?$displayName:$mail); + } + } + } + } + } + return $ret; + } + + /** + * This ajax method is used by the autocomplete function of the form element. + * + * @param[in] $data The address to the array of data witch will be return by the ajax request + * + * @retval void + **/ + public static function ajax_autocomplete(&$data) { + if ((isset($_REQUEST['attribute'])) && (isset($_REQUEST['objecttype'])) && (isset($_REQUEST['pattern'])) && (isset($_REQUEST['idform'])) ) { + if (LSsession ::loadLSobject($_REQUEST['objecttype'])) { + $object = new $_REQUEST['objecttype'](); + $form = $object -> getForm($_REQUEST['idform']); + $field=$form -> getElement($_REQUEST['attribute']); + $data['mails'] = $field -> autocomplete($_REQUEST['pattern']); + } + } + } + +} diff --git a/public_html/includes/js/LSformElement_mail.js b/public_html/includes/js/LSformElement_mail.js index 35678dd1..9edc8aeb 100644 --- a/public_html/includes/js/LSformElement_mail.js +++ b/public_html/includes/js/LSformElement_mail.js @@ -1,18 +1,19 @@ var LSformElement_mail = new Class({ initialize: function(){ + this.fields = []; this.initialiseLSformElement_mail(); if (typeof(varLSform) != "undefined") { varLSform.addModule("LSformElement_mail",this); } this.LSmail_open = 0; }, - + initialiseLSformElement_mail: function(el) { if (typeof(el) == 'undefined') { el = document; } el.getElements('input.LSformElement_mail').each(function(input) { - if (!input.hasClass('LSformElement_mail_disableMailSending')) { + if (!input.hasClass('LSformElement_mail_disableMailSending')) { this.addBtnAfter.bind(this)(input); } }, this); @@ -21,8 +22,18 @@ var LSformElement_mail = new Class({ this.addBtnAfter.bind(this)(a); } }, this); + var getName = /^(.*)\[\]$/; + el.getElements('input.LSformElement_mail_autocomplete').each(function(input) { + this.fields.push( + new LSformElement_mail_field( + getName.exec(input.name)[1], + input + ) + ); + }, this); + }, - + addBtnAfter: function(el) { var btn = new Element('img'); btn.setProperties({ @@ -33,12 +44,12 @@ var LSformElement_mail = new Class({ btn.addEvent('click',this.onBtnClick.bind(this,btn)); varLSdefault.addHelpInfo(btn,'LSformElement_mail','mail'); }, - + reinitialize: function(el) { varLSform.initializeModule('LSformElement_text',el); this.initialiseLSformElement_mail(el); }, - + onBtnClick: function(btn) { if (this.LSmail_open==0) { var mail = btn.getParent().getFirst().innerHTML; @@ -58,12 +69,12 @@ var LSformElement_mail = new Class({ } } }, - + onLSmailClose: function(LSmail) { LSdebug('LSformElement_mail : close LSmail'); this.LSmail_open = 0; }, - + onLSmailValid: function(LSmail) { LSdebug('LSformElement_mail : valid LSmail'); LSmail.send(); diff --git a/public_html/includes/js/LSformElement_mail_field.js b/public_html/includes/js/LSformElement_mail_field.js new file mode 100644 index 00000000..7579e833 --- /dev/null +++ b/public_html/includes/js/LSformElement_mail_field.js @@ -0,0 +1,157 @@ +var LSformElement_mail_field = new Class({ + initialize: function(name, input){ + this.name = name; + this.input = input; + this.ul = input.getParent('ul'); + this.li = input.getParent('li'); + this.keyUpTimer = null; + this.lastKeyUpValue = null; + this.lastAutocompletePattern = null; + this.lastAutocompleteMails = null; + this.initialiseLSformElement_mail_field(); + }, + + initialiseLSformElement_mail_field: function() { + this.input.addEvent('keyup',this.onKeyUp.bindWithEvent(this)); + this.input.addEvent('keydown',this.onKeyDown.bindWithEvent(this)); + }, + + onKeyDown: function(event) { + event = new Event(event); + if (event.key=='tab' && this.input.value) { + event.stop(); + if (this.keyUpTimer) { + clearTimeout(this.keyUpTimer); + } + this.launchAutocomplete(this.input.value); + } + }, + + onKeyUp: function(event) { + this.lastKeyUpValue = this.input.value; + if (this.keyUpTimer) { + clearTimeout(this.keyUpTimer); + } + if (this.lastKeyUpValue) { + this.keyUpTimer = this.onkeyUpTimeout.delay(800, this); + } + }, + + onkeyUpTimeout: function() { + this.keyUpTimer = null; + if (this.lastKeyUpValue == this.input.value) { + this.launchAutocomplete(this.input.value); + } + }, + + launchAutocomplete: function(pattern) { + if (this.lastAutocompletePattern == pattern) { + if (!this.autocompleteIsOpen()) this.showAutocompleteMails(); + return true; + } + this.input.set('disabled', 'disabled'); + this.lastAutocompletePattern=pattern; + var data = { + template: 'LSformElement_mail', + action: 'autocomplete', + attribute: this.name, + objecttype: varLSform.objecttype, + idform: varLSform.idform, + pattern: pattern + }; + data.imgload=varLSdefault.loadingImgDisplay(this.input); + new Request({url: 'index_ajax.php', data: data, onSuccess: this.onAutocompleteComplete.bind(this)}).send(); + }, + + onAutocompleteComplete: function(responseText, responseXML) { + var data = JSON.decode(responseText); + this.input.erase('disabled'); + if ( varLSdefault.checkAjaxReturn(data) ) { + this.lastAutocompleteMails = new Hash(data.mails); + this.showAutocompleteMails(); + } + }, + + showAutocompleteMails: function() { + if (!this.lastAutocompleteMails) return; + if (!$type(this.autocompleteUl)) { + this.autocompleteUl = new Element('ul'); + this.autocompleteUl.addClass('LSformElement_mail_autocomplete'); + this.autocompleteUl.injectInside(this.li); + document.addEvent('click', this.closeAutocompleteIfOpen.bind(this)); + } + this.autocompleteUl.empty(); + if (this.lastAutocompleteMails) { + this.lastAutocompleteMails.each(this.addAutocompleteLi, this); + } + this.addAutocompleteNoValueLabelIfEmpty(); + + this.autocompleteUl.setStyle('display','block'); + }, + + addAutocompleteLi: function(name, mail) { + var current = 0; + this.ul.getElements("input").each(function(input){ + if (input.value==mail && input != this.input) { + current=1; + } + },this); + + var li = new Element('li'); + li.addClass('LSformElement_mail_autocomplete'); + li.set('data-mail', mail); + li.set('html', name); + li.addEvent('mouseenter',this.onAutocompleteLiMouseEnter.bind(this,li)); + li.addEvent('mouseleave',this.onAutocompleteLiMouseLeave.bind(this,li)); + if (current) { + li.addClass('LSformElement_mail_autocomplete_current'); + } + else { + li.addEvent('click',this.onAutocompleteLiClick.bind(this,li)); + } + li.injectInside(this.autocompleteUl); + }, + + addAutocompleteNoValueLabelIfEmpty: function() { + if (this.autocompleteUl.getElement('li') == null) { + var li = new Element('li'); + li.addClass('LSformElement_mail_autocomplete'); + li.set('html', varLSdefault.LSjsConfig['LSformElement_mail_autocomplete_noResultLabel']); + li.injectInside(this.autocompleteUl); + } + }, + + onAutocompleteLiMouseEnter: function(li) { + li.addClass('LSformElement_mail_autocomplete_over'); + }, + + onAutocompleteLiMouseLeave: function(li) { + li.removeClass('LSformElement_mail_autocomplete_over'); + }, + + onAutocompleteLiClick: function(li) { + this.closeAutocomplete(); + if (li.get('data-mail')) { + this.input.value = li.get('data-mail'); + } + }, + + autocompleteIsOpen: function() { + return ($type(this.autocompleteUl) == 'element' && this.autocompleteUl.getStyle('display') != 'none'); + }, + + closeAutocomplete: function() { + if (!this.autocompleteIsOpen()) return true; + this.autocompleteUl.setStyle('display', 'none'); + }, + + closeAutocompleteIfOpen: function(event) { + event = new Event(event); + if (!this.autocompleteIsOpen()) + return true; + if (event.target==this.input || event.target==this.autocompleteUl) + return true; + this.closeAutocomplete(); + }, + +});