2 commits - lib/client lib/kolab_client_task.php lib/kolab_form.php lib/kolab_html.php lib/kolab_utils.php lib/locale public_html/js public_html/skins

Aleksander Machniak machniak at kolabsys.com
Wed Mar 21 15:04:39 CET 2012


 lib/client/kolab_client_task_group.php       |   34 +++
 lib/client/kolab_client_task_main.php        |    3 
 lib/kolab_client_task.php                    |   23 +-
 lib/kolab_form.php                           |    4 
 lib/kolab_html.php                           |    7 
 lib/kolab_utils.php                          |    9 
 lib/locale/en_US.php                         |    3 
 public_html/js/kolab_admin.js                |  296 ++++++++++++++++++++++++++-
 public_html/skins/default/images/buttons.png |binary
 public_html/skins/default/style.css          |   73 ++++++
 public_html/skins/default/ui.js              |  161 +++++++++++---
 11 files changed, 564 insertions(+), 49 deletions(-)

New commits:
commit 50492d4dab7adb56e6830b52dfca278685e316fc
Author: Aleksander Machniak <alec at alec.pl>
Date:   Wed Mar 21 15:03:58 2012 +0100

    Fix users search (#641)

diff --git a/public_html/js/kolab_admin.js b/public_html/js/kolab_admin.js
index f5075e2..63189a7 100644
--- a/public_html/js/kolab_admin.js
+++ b/public_html/js/kolab_admin.js
@@ -676,7 +676,7 @@ function kolab_admin()
     var i, v, json = {},
       form = $(id),
       query = form.serializeArray(),
-      extra = this.env.extra_fields;
+      extra = this.env.extra_fields ? this.env.extra_fields : [];
 
     for (i in query)
       json[query[i].name] = query[i].value;


commit 9117d4f73fe8daf847864ce422df93c3c1e4a0e8
Author: Aleksander Machniak <alec at alec.pl>
Date:   Wed Mar 21 14:56:31 2012 +0100

    Autocompletion list widget

diff --git a/lib/client/kolab_client_task_group.php b/lib/client/kolab_client_task_group.php
index 3d8c876..821d438 100644
--- a/lib/client/kolab_client_task_group.php
+++ b/lib/client/kolab_client_task_group.php
@@ -261,6 +261,27 @@ class kolab_client_task_group extends kolab_client_task
             );
         }
 
+        // Members (get member names)
+        if (!empty($data['group'])) {
+            // find members attribute name
+            foreach (array('member', 'uniquemember') as $attr) {
+                if (isset($fields[$attr]) && isset($data[$attr])) {
+                    $attr_name = $attr;
+                }
+            }
+            if (!empty($attr_name)) {
+                $result = $this->api->get('group.members_list', array('group' => $data['group']));
+                $list   = (array) $result->get('list');
+                $data[$attr_name] = $this->parse_members($list);
+            }
+        }
+
+$fields['debug'] = array(
+    'label' => 'debug',
+    'section' => 'system',
+    'value' => '<pre>'.kolab_html::escape(print_r($data, true)).'</pre>',
+);
+
         // Create form object and populate with fields
         $form = $this->form_create('group', $attribs, $sections, $fields, $fields_map, $data);
 
@@ -271,6 +292,19 @@ class kolab_client_task_group extends kolab_client_task
         return $form->output();
     }
 
+    private function parse_members($list)
+    {
+        // convert to key=>value array, see kolab_api_service_form_value::list_options_uniquemember()
+        foreach ($list as $idx => $value) {
+            $list[$idx] = $value['displayname'];
+            if (!empty($value['mail'])) {
+                $list[$idx] .= ' <' . $value['mail'] . '>';
+            }
+        }
+
+        return $list;
+    }
+
     /**
      * Returns list of group types.
      *
diff --git a/lib/client/kolab_client_task_main.php b/lib/client/kolab_client_task_main.php
index 9996526..b7b4621 100644
--- a/lib/client/kolab_client_task_main.php
+++ b/lib/client/kolab_client_task_main.php
@@ -40,7 +40,8 @@ class kolab_client_task_main extends kolab_client_task
         $this->output->set_env('watermark', $this->output->get_template('watermark'));
 
         // assign default set of translations
-        $this->output->add_translation('loading', 'saving', 'deleting', 'servererror', 'search');
+        $this->output->add_translation('loading', 'saving', 'deleting', 'servererror',
+            'search', 'search.loading', 'search.acchars');
 
         // Create list of tasks for dashboard
         // @TODO: check capabilities
diff --git a/lib/kolab_client_task.php b/lib/kolab_client_task.php
index bd49cd3..6ee696f 100644
--- a/lib/kolab_client_task.php
+++ b/lib/kolab_client_task.php
@@ -618,6 +618,9 @@ class kolab_client_task
             if (!empty($field['maxlength'])) {
                 $result['data-maxlength'] = $field['maxlength'];
             }
+            if (!empty($field['autocomplete'])) {
+                $result['data-autocomplete'] = true;
+            }
             break;
 
         default:
@@ -781,6 +784,7 @@ class kolab_client_task
         }
 
         $form = new kolab_form($attribs);
+        $assoc_fields = array();
 
         // Parse elements and add them to the form object
         foreach ($sections as $section_idx => $section) {
@@ -792,20 +796,24 @@ class kolab_client_task
                 }
 
                 if (empty($field['label'])) {
-                    $field['label'] = "user.$idx";
+                    $field['label'] = "$name.$idx";
                 }
 
                 $field['label']       = kolab_html::escape($this->translate($field['label']));
-                $field['description'] = "user.$idx.desc";
+                $field['description'] = "$name.$idx.desc";
                 $field['section']     = $section_idx;
 
                 if (!empty($data[$idx])) {
-                    if (is_array($data[$idx])) {
-                        $field['value'] = array_map(array('kolab_html', 'escape'), $data[$idx]);
-                        $field['value'] = implode("\n", $field['value']);
+                    $field['value'] = $data[$idx];
+
+                    // Convert data for the list field with autocompletion
+                    if ($field['data-type'] == kolab_form::TYPE_LIST && kolab_utils::is_assoc($data[$idx])) {
+                        $assoc_fields[$idx] = $data[$idx];
+                        $field['value'] = array_keys($data[$idx]);
                     }
-                    else {
-                        $field['value'] = kolab_html::escape($data[$idx]);
+
+                    if (is_array($field['value'])) {
+                        $field['value'] = implode("\n", $field['value']);
                     }
                 }
 /*
@@ -858,6 +866,7 @@ class kolab_client_task
         }
 
         $this->output->set_env('form_id', $attribs['id']);
+        $this->output->set_env('assoc_fields', $assoc_fields);
 
         return $form;
     }
diff --git a/lib/kolab_form.php b/lib/kolab_form.php
index c78486f..850ca8f 100644
--- a/lib/kolab_form.php
+++ b/lib/kolab_form.php
@@ -34,6 +34,7 @@ class kolab_form
     const INPUT_SUBMIT = 7;
     const INPUT_SELECT = 8;
     const INPUT_HIDDEN = 9;
+    const INPUT_CUSTOM = 10;
 
     const TYPE_LIST = 1;
 
@@ -279,9 +280,10 @@ class kolab_form
             break;
 
         case self::INPUT_SELECT:
-            $content = kolab_html::select($attribs);
+            $content = kolab_html::select($attribs, true);
             break;
 
+        case self::INPUT_CUSTOM:
         default:
             if (is_array($attribs)) {
                 $content = isset($attribs['value']) ? $attribs['value'] : '';
diff --git a/lib/kolab_html.php b/lib/kolab_html.php
index afce918..9680ef5 100644
--- a/lib/kolab_html.php
+++ b/lib/kolab_html.php
@@ -316,6 +316,13 @@ class kolab_html
 
     public static function escape($value)
     {
+        if (is_array($value)) {
+            foreach ($value as $idx => $val) {
+                $value[$idx] = self::escape($val);
+            }
+            return $value;
+        }
+
         return htmlspecialchars($value, ENT_COMPAT, KADM_CHARSET);
     }
 }
diff --git a/lib/kolab_utils.php b/lib/kolab_utils.php
index 2694f71..b80bc6a 100644
--- a/lib/kolab_utils.php
+++ b/lib/kolab_utils.php
@@ -142,4 +142,13 @@ class kolab_utils
 
         return false;
     }
+
+    /**
+     * Finds wether an array is associative or not.
+     */
+    public static function is_assoc ($arr)
+    {
+        return is_array($arr) && count(array_filter(array_keys($arr), 'is_string')) == count($arr);
+    }
+
 }
diff --git a/lib/locale/en_US.php b/lib/locale/en_US.php
index efd7439..c0bbbf1 100644
--- a/lib/locale/en_US.php
+++ b/lib/locale/en_US.php
@@ -25,6 +25,8 @@ $LANG['search.prefix'] = 'begins with';
 $LANG['search.name'] = 'name';
 $LANG['search.email'] = 'email';
 $LANG['search.uid'] = 'UID';
+$LANG['search.loading'] = 'Searching...';
+$LANG['search.acchars'] = 'At least $min characters required for autocompletion';
 
 $LANG['menu.users'] = 'Users';
 $LANG['menu.groups'] = 'Groups';
@@ -98,6 +100,7 @@ $LANG['group.group_type_id'] = 'Group type';
 $LANG['group.add.success'] = 'Group created successfully.';
 $LANG['group.delete.success'] = 'Group deleted successfully.';
 $LANG['group.gidnumber'] = 'Primary group number';
+$LANG['group.uniquemember'] = 'Members';
 $LANG['group.system'] = 'System';
 $LANG['group.other'] = 'Other';
 
diff --git a/public_html/js/kolab_admin.js b/public_html/js/kolab_admin.js
index bdcf1e2..f5075e2 100644
--- a/public_html/js/kolab_admin.js
+++ b/public_html/js/kolab_admin.js
@@ -38,7 +38,6 @@ function kolab_admin()
     beforeSend: function(xmlhttp) { xmlhttp.setRequestHeader('X-Session-Token', ref.env.token); }
   });
 
-
   /*********************************************************/
   /*********          basic utilities              *********/
   /*********************************************************/
@@ -357,7 +356,7 @@ function kolab_admin()
       this.display_message(msg, 'error');
 
       // Logout on invalid-session error
-      if (response.code == 403)
+      if (response && response.code == 403)
         this.main_logout();
 
       return false;
@@ -412,6 +411,262 @@ function kolab_admin()
     }
   };
 
+
+  /*********************************************************/
+  /*********     keyboard autocomplete methods     *********/
+  /*********************************************************/
+
+  this.ac_init = function(obj, props)
+  {
+    obj.keydown(function(e) { return kadm.ac_keydown(e, props); })
+      .attr('autocomplete', 'off');
+  };
+
+  // handler for keyboard events on autocomplete-fields
+  this.ac_keydown = function(e, props)
+  {
+    if (this.ac_timer)
+      clearTimeout(this.ac_timer);
+
+    var highlight, key = e.which;
+
+    switch (key) {
+      case 38:  // arrow up
+      case 40:  // arrow down
+        if (!this.ac_visible())
+          break;
+
+        var dir = key == 38 ? 1 : 0;
+
+        highlight = $('.selected', this.ac_pane).get(0);
+
+        if (!highlight)
+          highlight = this.ac_pane.__ul.firstChild;
+
+        if (highlight)
+          this.ac_select(dir ? highlight.previousSibling : highlight.nextSibling);
+
+        return e.stopPropagation();
+
+      case 9:   // tab
+        if (e.shiftKey || !this.ac_visible()) {
+          this.ac_stop();
+          return;
+        }
+
+      case 13:  // enter
+        if (!this.ac_visible())
+          return false;
+
+        // insert selected item and hide selection pane
+        this.ac_insert(this.ac_selected);
+        this.ac_stop();
+
+        return e.stopPropagation();
+
+      case 27:  // escape
+        this.ac_stop();
+        return;
+
+      case 37:  // left
+      case 39:  // right
+        if (!e.shiftKey)
+	      return;
+    }
+
+    // start timer
+    this.ac_timer = window.setTimeout(function() { kadm.ac_start(props); }, 200);
+    this.ac_input = e.target;
+
+    return true;
+  };
+
+  this.ac_visible = function()
+  {
+    return (this.ac_selected !== null && this.ac_selected !== undefined && this.ac_value);
+  };
+
+  this.ac_select = function(node)
+  {
+    if (!node)
+      return;
+
+    var current = $('.selected', this.ac_pane);
+
+    if (current.length)
+      current.removeClass('selected');
+
+    $(node).addClass('selected');
+    this.ac_selected = node._id;
+  };
+
+  // autocomplete search processor
+  this.ac_start = function(props)
+  {
+    var q = this.ac_input ? this.ac_input.value : null,
+      min = this.env.autocomplete_min_length,
+      old_value = this.ac_value,
+      ac = this.ac_data;
+
+    if (q === null)
+      return;
+
+    // trim query string
+    q = $.trim(q);
+
+    // Don't (re-)search if the last results are still active
+    if (q == old_value)
+      return;
+
+    // Stop and destroy last search
+    this.ac_stop();
+
+    if (q.length && q.length < min) {
+      if (!this.ac_info) {
+        this.ac_info = this.display_message(
+          this.t('search.acchars').replace('$min', min));
+      }
+      return;
+    }
+
+    this.ac_value = q;
+
+    // ...string is empty
+    if (!q.length)
+      return;
+
+    // ...new search value contains old one, but the old result was empty
+    if (old_value && old_value.length && q.indexOf(old_value) == 0 && this.ac_result && !this.ac_result.length)
+      return;
+
+    var i, xhr, data = props,
+      action = props && props.action ? props.action : 'form_value.list_options';
+
+    this.ac_oninsert = props.oninsert;
+    data.search = q;
+    delete data['action'];
+    delete data['insert_func'];
+
+    this.display_message(this.t('search.loading'), 'loading');
+    xhr = this.api_post(action, data, 'ac_result');
+    this.ac_data = xhr;
+  };
+
+  this.ac_result = function(response)
+  {
+    // search stopped in meantime?
+    if (!this.ac_value)
+      return;
+
+    if (!this.api_response(response))
+      return;
+
+    // ignore this outdated search response
+    if (this.ac_input && response.result.search != this.ac_value)
+      return;
+
+    // display search results
+    var i, ul, li, text,
+      result = response.result.list,
+      pos = $(this.ac_input).offset(),
+      value = this.ac_value,
+      rx = new RegExp('(' + RegExp.escape(value) + ')', 'ig');
+
+    // create results pane if not present
+    if (!this.ac_pane) {
+      ul = $('<ul>');
+      this.ac_pane = $('<div>').attr('id', 'autocompletepane')
+        .css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
+      this.ac_pane.__ul = ul[0];
+    }
+
+    ul = this.ac_pane.__ul;
+
+    // reset content
+    ul.innerHTML = '';
+    // move the results pane right under the input box
+    this.ac_pane.css({left: (pos.left - 1)+'px', top: (pos.top + this.ac_input.offsetHeight - 1)+'px', display: 'none'});
+
+    // add each result line to the list
+    for (i in result) {
+      text = result[i];
+      li = document.createElement('LI');
+      li.innerHTML = text.replace(rx, '##$1%%').replace(/</g, '<').replace(/>/g, '>').replace(/##([^%]+)%%/g, '<b>$1</b>');
+      li.onmouseover = function() { kadm.ac_select(this); };
+      li.onmouseup = function() { kadm.ac_click(this) };
+      li._id = i;
+      ul.appendChild(li);
+    }
+
+    if (ul.childNodes.length) {
+      this.ac_pane.show();
+
+      // select the first
+      li = $('li:first', ul);
+      li.addClass('selected');
+      this.ac_selected = li.get(0)._id;
+    }
+
+    this.env.ac_result = result;
+  };
+
+  this.ac_click = function(node)
+  {
+    if (this.ac_input)
+      this.ac_input.focus();
+
+    this.ac_insert(node._id);
+    this.ac_stop();
+  };
+
+  this.ac_insert = function(id)
+  {
+    var val = this.env.ac_result[id];
+
+    if (typeof this.ac_oninsert == 'function')
+      this.ac_oninsert(id, val);
+    else
+      $(this.ac_input).val(val);
+  };
+
+  this.ac_blur = function()
+  {
+    if (this.ac_timer)
+      clearTimeout(this.ac_timer);
+
+    this.ac_input = null;
+    this.ac_stop();
+  };
+
+  this.ac_stop = function()
+  {
+    this.ac_selected = null;
+    this.ac_value = '';
+
+    if (this.ac_pane)
+      this.ac_pane.hide();
+
+    this.ac_destroy();
+  };
+
+  // Clears autocomplete data/requests
+  this.ac_destroy = function()
+  {
+    if (this.ac_data)
+      this.ac_data.abort();
+
+    if (this.ac_info)
+      this.hide_message(this.ac_info);
+
+    if (this.ac_msg)
+      this.hide_message(this.ac_msg);
+
+    this.ac_data = null;
+    this.ac_info = null;
+    this.ac_msg = null;
+  };
+
+
   /*********************************************************/
   /*********                 Forms                 *********/
   /*********************************************************/
@@ -615,9 +870,32 @@ function kolab_admin()
     this.command('group.list', {page: page});
   };
 
-};
+  this.group_save = function(reload, section)
+  {
+    var data = this.serialize_form('#'+this.env.form_id);
 
-var kadm = new kolab_admin();
+    if (reload) {
+      data.section = section;
+      this.http_post('group.add', {data: data});
+      return;
+    }
+
+    this.form_error_clear();
+
+    this.set_busy(true, 'saving');
+    this.api_post('group.add', data, 'group_save_response');
+  };
+
+  this.group_save_response = function(response)
+  {
+    if (!this.api_response(response))
+      return;
+
+    this.display_message('group.add.success');
+    this.command('group.list', {page: this.env.list_page});
+  };
+
+};
 
 // Add escape() method to RegExp object
 // http://dev.rubyonrails.org/changeset/7271
@@ -625,3 +903,11 @@ RegExp.escape = function(str)
 {
   return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
 };
+
+var kadm = new kolab_admin();
+
+// general click handler
+$(document).click(function() {
+  // destroy autocompletion
+  kadm.ac_stop();
+});
diff --git a/public_html/skins/default/images/buttons.png b/public_html/skins/default/images/buttons.png
index 0492d09..586851e 100644
Binary files a/public_html/skins/default/images/buttons.png and b/public_html/skins/default/images/buttons.png differ
diff --git a/public_html/skins/default/style.css b/public_html/skins/default/style.css
index 96cfa30..bb52537 100644
--- a/public_html/skins/default/style.css
+++ b/public_html/skins/default/style.css
@@ -25,6 +25,7 @@ textarea {
   -moz-border-radius: 3px;
   -webkit-border-radius: 3px;
   padding-left: 2px;
+  color: black;
 }
 
 table.list {
@@ -494,6 +495,9 @@ textarea.readonly {
 span.listarea {
   display: block;
   width: 370px;
+  max-height: 209px;
+  overflow-y: auto;
+  overflow-x: hidden;
   margin: 0 0 1px;
   padding: 0;
   background-color: white;
@@ -520,7 +524,8 @@ span.listelement input {
   border: none;
   background-color: transparent;
   padding-left: 2px;
-  width: 330px;
+  /* FIXME: it should be 330px, but when listarea has scroller, input jumps below action buttons */
+  width: 314px;
   height: 19px;
 }
 
@@ -561,18 +566,84 @@ span.listelement span.actions span.add {
   background-position: -43px 0;
 }
 
+span.listelement span.actions span.search {
+  background-position: -65px 0;
+  cursor: default;
+}
+
 span.listarea.readonly {
   background-color: #f5f5f5;
 }
 
+input.readonly,
 span.listarea.readonly span.listelement input {
   color: #a0a0a0;
+  cursor: default;
+}
+
+span.listarea.autocomplete span.listelement input {
+  color: #514949;
+}
+
+span.listarea.autocomplete span.listelement input.autocomplete {
+  color: black;
 }
 
 span.listarea.readonly span.listelement span.actions {
   opacity: .5;
 }
 
+.autocomplete > span.listelement input {
+  /* FIXME: it should be 348px, but when listarea has scroller, input jumps below action buttons */
+  width: 332px;
+}
+
+.autocomplete > span.listelement span.actions {
+  width: 18px;
+}
+
+.autocomplete > span.listelement span.actions span.reset {
+  border-left: none;
+}
+
+.autocomplete > span.listelement span.actions span.search:hover {
+  background-color: #f0f0f0;
+}
+
+/*****   autocomplete list   *****/
+
+#autocompletepane
+{
+  background-color: white;
+  border: 1px solid #d0d0d0;
+  min-width: 351px;
+}
+
+#autocompletepane ul
+{
+  margin: 0px;
+  padding: 2px;
+  list-style-image: none;
+  list-style-type: none;
+}
+
+#autocompletepane ul li
+{
+  display: block;
+  height: 16px;
+  font-size: 11px;
+  padding-left: 6px;
+  padding-top: 2px;
+  padding-right: 6px;
+  white-space: nowrap;
+  cursor: pointer;
+}
+
+#autocompletepane ul li.selected
+{
+  background-color: #d6efff;
+}
+
 /***** tabbed interface elements *****/
 
 div.tabsbar
diff --git a/public_html/skins/default/ui.js b/public_html/skins/default/ui.js
index 850eb93..32f5dfc 100644
--- a/public_html/skins/default/ui.js
+++ b/public_html/skins/default/ui.js
@@ -143,7 +143,7 @@ function form_serialize(data)
   // replace some textarea fields with pretty/smart input lists
   $('textarea[data-type="list"]', form).not('disabled').each(function() {
     var i, v, value = [],
-      re = RegExp('^' + RegExp.escape(this.name) + '\[[0-9]+\]$');
+      re = RegExp('^' + RegExp.escape(this.name) + '\[[0-9-]+\]$');
 
     for (i in data.json) {
       if (i.match(re)) {
@@ -152,6 +152,14 @@ function form_serialize(data)
         delete data.json[i];
       }
     }
+
+    // autocompletion lists data is stored in env variable
+    if (kadm.env.assoc_fields[this.name]) {
+      value = [];
+      for (i in kadm.env.assoc_fields[this.name])
+        value.push(i);
+    }
+
     data.json[this.name] = value;
   });
 
@@ -187,67 +195,152 @@ function form_init(id)
 // Replaces form element with smart element
 function form_element_wrapper(form_element)
 {
-  var i, len, elem, e = $(form_element),
-    list = form_element.value.split("\n"),
-    area = $('<span class="listarea"></span>'),
-    disabled = e.attr('disabled') || e.attr('readonly');
+  var i, j = 0, len, elem, e = $(form_element),
+    list = kadm.env.assoc_fields[form_element.name],
+    disabled = e.attr('disabled') || e.attr('readonly'),
+    autocomplete = e.attr('data-autocomplete'),
+    maxlength = e.attr('data-maxlength'),
+    area = $('<span class="listarea"></span>');
 
   e.hide();
 
-  for (i=0, len=list.length; i<len; i++) {
+  // add autocompletion input
+  if (!disabled && autocomplete) {
+    elem = form_list_element(form_element.form, {
+      maxlength: maxlength,
+      autocomplete: autocomplete,
+      element: e
+    }, -1);
+
+    elem.appendTo(area);
+    kadm.ac_init(elem, {attribute: form_element.name, oninsert: form_element_insert_func});
+  }
+
+  if (!list && form_element.value)
+    list = $.extend({}, form_element.value.split("\n"));
+
+  // add input rows
+  for (i in list) {
     elem = form_list_element(form_element.form, {
-      name: form_element.name+'['+i+']',
       value: list[i],
+      key: i,
       disabled: disabled,
-      maxlength: e.attr('data-maxlength')
-    });
+      maxlength: maxlength,
+      autocomplete: autocomplete,
+      element: e
+    }, j++);
+
     elem.appendTo(area);
   }
 
   if (disabled)
     area.addClass('readonly');
+  if (autocomplete)
+    area.addClass('autocomplete');
 
   area.appendTo(form_element.parentNode);
 }
 
 // Creates smart list element
-function form_list_element(form, data)
+function form_list_element(form, data, idx)
 {
-  var elem = $('<span class="listelement"><span class="actions">'
-    + '<span title="" class="add"></span><span title="" class="reset"></span>'
-    + '</span><input></span>');
+  var content, elem, input,
+    key = data.key,
+    orig = data.element
+    ac = data.autocomplete;
+
+  data.name = data.name || orig.attr('name') + '[' + idx + ']';
+  data.disabled = data.disabled || (ac && idx >= 0);
+  data.readonly = data.readonly || (ac && idx >= 0);
+
+  // remove internal attributes
+  delete data['element'];
+  delete data['autocomplete'];
+  delete data['key'];
+
+  // build element content
+  content = '<span class="listelement"><span class="actions">'
+    + (!ac ? '<span title="" class="add"></span>' : ac && idx == -1 ? '<span title="" class="search"></span>' : '')
+    + (!ac || idx >= 0 ? '<span title="" class="reset"></span>' : '')
+    + '</span><input></span>';
+
+  elem = $(content);
+  input = $('input', elem);
+
+  // Set INPUT attributes
+  input.attr(data);
 
-  $('input', elem).attr(data);
+  if (data.readonly)
+    input.addClass('readonly');
 
-  if (data.disabled)
+  if (ac && idx == -1)
+    input.addClass('autocomplete');
+
+  if (data.disabled && !ac)
     return elem;
 
   // attach element creation event
-  $('span[class="add"]', elem).click(function() {
-    var dt = (new Date()).getTime(),
-      span = $(this.parentNode.parentNode),
-      name = data.name.replace(/\[[0-9]+\]$/, ''),
-      elem = form_list_element(form, {name: name+'['+dt+']'});
-
-    span.after(elem);
-    $('input', elem).focus();
-  });
+  if (!ac)
+    $('span[class="add"]', elem).click(function() {
+      var dt = (new Date()).getTime(),
+        span = $(this.parentNode.parentNode),
+        name = data.name.replace(/\[[0-9]+\]$/, ''),
+        elem = form_list_element(form, {name: name}, dt);
+
+      span.after(elem);
+      $('input', elem).focus();
+      kadm.ac_stop();
+    });
 
   // attach element deletion event
-  $('span[class="reset"]', elem).click(function() {
-    var l, span = $(this.parentNode.parentNode),
-      name = data.name.replace(/\[[0-9]+\]$/, ''),
-      l = $('input[name^="' + name + '"]', form);
-
-    if (l.length > 1)
-      span.remove();
-    else
-      $('input', span).val('').focus();
-  });
+  if (!ac || idx >= 0)
+    $('span[class="reset"]', elem).click(function() {
+      var span = $(this.parentNode.parentNode),
+        name = data.name.replace(/\[[0-9]+\]$/, ''),
+        l = $('input[name^="' + name + '"]', form),
+        key = $(this).data('key');
+
+      if (ac || l.length > 1)
+        span.remove();
+      else
+        $('input', span).val('').focus();
+
+      // delete key from internal field representation
+      if (key !== undefined && kadm.env.assoc_fields[name])
+        delete kadm.env.assoc_fields[name][key];
+
+      kadm.ac_stop();
+    }).data('key', key);
 
   return elem;
 }
 
+function form_element_insert_func(key, val)
+{
+  var elem, input = $(this.ac_input).get(0),
+    dt = (new Date()).getTime(),
+    span = $(input.parentNode),
+    name = input.name.replace(/\[-1\]$/, '');
+
+  // reset autocomplete input
+  input.value = '';
+
+  // check if element doesn't exist on the list already
+  if (kadm.env.assoc_fields[name][key])
+    return;
+
+  // add element
+  elem = form_list_element(input.form, {
+    name: name,
+    autocomplete: true,
+    value: val
+    }, dt);
+  span.after(elem);
+
+  // update field variable
+  kadm.env.assoc_fields[name][key] = val;
+}
+
 /**
  * UI Initialization
  */





More information about the commits mailing list