Procházet zdrojové kódy

Improved implementation of the 'autocomplete' input and fix of quite a few related issue with aysnchronous inputs. Autocompletes are now restricted to external keys only.
Some details:
- Autocomplete now matches on 'contains' instead of 'begins with'
- The minimum size of this match is configurable in the config file and per attribute ('min_autocomplete_chars').
- The maximum size that turns a drop-down list into an autocomplete is configurable in the config-file and per attribute ('max_combo_length').
- Better feedback when expanding/collapsing search results lists.
- 'Pointer' cursor on the link to Expand/Collapse results lists.
- The 'mandatory' state of an attribute is no longer lost when some part of a form is reloaded asynchronously

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@947 a333f486-631f-4898-b8df-5754b55c2be0

dflaven před 14 roky
rodič
revize
54fc60997f

+ 12 - 0
application/ajaxwebpage.class.inc.php

@@ -60,6 +60,7 @@ class ajax_page extends WebPage
         $s_captured_output = ob_get_contents();
         ob_end_clean();
         echo $this->s_content;
+        echo $this->s_deferred_content;
         if (!empty($this->m_sReadyScript))
         {
 	        echo "<script>\n";
@@ -94,6 +95,17 @@ class ajax_page extends WebPage
 		// considering that at this time everything in the page is "ready"...
 		$this->m_sReadyScript .= $sScript;
 	}
+	
+	/**
+	 * Cannot be called in this context, since Ajax pages do not share
+	 * any context with the calling page !!
+	 */
+	public function GetUniqueId()
+	{
+		assert(false);
+		return 0;
+	}
+	
 }
 
 ?>

+ 84 - 60
application/cmdbabstract.class.inc.php

@@ -36,6 +36,7 @@ require_once('../application/utils.inc.php');
 require_once('../application/applicationcontext.class.inc.php');
 require_once('../application/ui.linkswidget.class.inc.php');
 require_once('../application/ui.passwordwidget.class.inc.php');
+require_once('../application/ui.autocompletewidget.class.inc.php');
 require_once('../application/ui.htmleditorwidget.class.inc.php');
 
 abstract class cmdbAbstractObject extends CMDBObject
@@ -385,8 +386,14 @@ abstract class cmdbAbstractObject extends CMDBObject
 	 */	
 	public static function GetDisplaySet(WebPage $oPage, CMDBObjectSet $oSet, $aExtraParams = array())
 	{
-		static $iListId = 0;
-		$iListId++;
+		if (empty($aExtraParams['currentId']))
+		{
+			$iListId = $oPage->GetUniqueId(); // Works only if not in an Ajax page !!
+		}
+		else
+		{
+			$iListId = $aExtraParams['currentId'];
+		}
 		
 		// Initialize and check the parameters
 		$bViewLink = isset($aExtraParams['view_link']) ? $aExtraParams['view_link'] : true;
@@ -526,14 +533,14 @@ abstract class cmdbAbstractObject extends CMDBObject
 		}
 		$sHtml .= '<table class="listContainer">';
 		$sColspan = '';
-		if (isset($aExtraParams['block_id']))
-		{
-			$divId = $aExtraParams['block_id'];
-		}
-		else
-		{
-			$divId = 'missingblockid';
-		}
+//		if (isset($aExtraParams['block_id']))
+//		{
+//			$divId = $aExtraParams['block_id'];
+//		}
+//		else
+//		{
+//			$divId = 'missingblockid';
+//		}
 		$sFilter = $oSet->GetFilter()->serialize();
 		$iMinDisplayLimit = utils::GetConfig()->GetMinDisplayLimit();
 		$sCollapsedLabel = Dict::Format('UI:TruncatedResults', $iMinDisplayLimit, $oSet->Count());
@@ -546,11 +553,11 @@ abstract class cmdbAbstractObject extends CMDBObject
 		{
 			// list truncated
 			$aExtraParams['display_limit'] = true;
-			$sHtml .= '<tr class="containerHeader"><td><span id="lbl_'.$divId.'">'.$sCollapsedLabel.'</span>&nbsp;&nbsp;<a class="truncated" id="trc_'.$divId.'">'.$sLinkLabel.'</a></td><td>';
+			$sHtml .= '<tr class="containerHeader"><td><span id="lbl_'.$iListId.'">'.$sCollapsedLabel.'</span>&nbsp;&nbsp;<a class="truncated" id="trc_'.$iListId.'">'.$sLinkLabel.'</a></td><td>';
 			$oPage->add_ready_script(
 <<<EOF
-	$('#$divId table.listResults').addClass('truncated');
-	$('#$divId table.listResults tr:last td').addClass('truncated');
+	$('#$iListId table.listResults').addClass('truncated');
+	$('#$iListId table.listResults tr:last td').addClass('truncated');
 EOF
 );
 		}
@@ -558,14 +565,14 @@ EOF
 		{
 			// Collapsible list
 			$aExtraParams['display_limit'] = true;
-			$sHtml .= '<tr class="containerHeader"><td><span id="lbl_'.$divId.'">'.Dict::Format('UI:CountOfResults', $oSet->Count()).'</span><a class="truncated" id="trc_'.$divId.'">'.Dict::S('UI:CollapseList').'</a></td><td>';
+			$sHtml .= '<tr class="containerHeader"><td><span id="lbl_'.$iListId.'">'.Dict::Format('UI:CountOfResults', $oSet->Count()).'</span><a class="truncated" id="trc_'.$iListId.'">'.Dict::S('UI:CollapseList').'</a></td><td>';
 		}
 		$aExtraParams['truncated'] = false; // To expand the full list when clicked
 		$sExtraParamsExpand = addslashes(str_replace('"', "'", json_encode($aExtraParams))); // JSON encode, change the style of the quotes and escape them
 		$oPage->add_ready_script(
 <<<EOF
 	// Handle truncated lists
-	$('#trc_$divId').click(function()
+	$('#trc_$iListId').click(function()
 	{
 		var state = {};
 		
@@ -582,15 +589,15 @@ EOF
 		$.bbq.pushState( state );
 		$(this).trigger(state[this.id]);	
 	});
-	
-	$('#trc_$divId').bind('open', function()
+	$('#trc_$iListId').unbind('open');
+	$('#trc_$iListId').bind('open', function()
 	{
-		ReloadTruncatedList('$divId', '$sFilter', '$sExtraParamsExpand');
+		ReloadTruncatedList('$iListId', '$sFilter', '$sExtraParamsExpand');
 	});
-	
-	$('#trc_$divId').bind('close', function()
+	$('#trc_$iListId').unbind('close');	
+	$('#trc_$iListId').bind('close', function()
 	{
-		TruncateList('$divId', $iMinDisplayLimit, '$sCollapsedLabel', '$sLinkLabel');
+		TruncateList('$iListId', $iMinDisplayLimit, '$sCollapsedLabel', '$sLinkLabel');
 	});
 EOF
 );
@@ -604,7 +611,7 @@ EOF
 				//$aMenuExtraParams['linkage'] = $sLinkageAttribute;
 				$aMenuExtraParams = $aExtraParams;
 			}
-			$sHtml .= $oMenuBlock->GetRenderContent($oPage, $aMenuExtraParams);
+			$sHtml .= $oMenuBlock->GetRenderContent($oPage, $aMenuExtraParams, $iListId);
 			$sHtml .= '</td></tr>';
 		}
 		$sHtml .= "<tr><td $sColspan>";
@@ -872,13 +879,12 @@ EOF
 		if (isset($aExtraParams['currentId']))
 		{
 			$sSearchFormId = $aExtraParams['currentId'];
-			$iSearchFormId++;
 		}
 		else
 		{
-			$iSearchFormId++;
+			$iSearchFormId = $oPage->GetUniqueId();
 			$sSearchFormId = 'SimpleSearchForm'.$iSearchFormId;
-			$sHtml .= "<div id=\"$sSearchFormId\" class=\"mini_tab{$iSearchFormId}\">\n";			
+			$sHtml .= "<div id=\"ds_$sSearchFormId\" class=\"mini_tab{$iSearchFormId}\">\n";			
 		}
 		// Check if the current class has some sub-classes
 		if (isset($aExtraParams['baseClass']))
@@ -908,7 +914,7 @@ EOF
 			$sClassesCombo = MetaModel::GetName($sClassName);
 		}
 		$oUnlimitedFilter = new DBObjectSearch($sClassName);
-		$sHtml .= "<form id=\"form{$iSearchFormId}\" action=\"../pages/UI.php\">\n"; // Don't use $_SERVER['SCRIPT_NAME'] since the form may be called asynchronously (from ajax.php)
+		$sHtml .= "<form id=\"fs_{$sSearchFormId}\" action=\"../pages/UI.php\">\n"; // Don't use $_SERVER['SCRIPT_NAME'] since the form may be called asynchronously (from ajax.php)
 		$sHtml .= "<h2>".Dict::Format('UI:SearchFor_Class_Objects', $sClassesCombo)."</h2>\n";
 		$index = 0;
 		$sHtml .= "<p>\n";
@@ -1042,7 +1048,7 @@ EOF
 		}
 		else
 		{
-			$iInputId++;
+			$oPoage->GetUniqueId();
 			$iId = $iInputId;
 		}
 
@@ -1120,6 +1126,44 @@ EOF
 					// Event list & validation is handled  directly by the widget
 				break;
 				
+				case 'ExtKey':
+					$aEventsList[] ='validate';
+					$aEventsList[] ='change';
+
+					// #@# todo - add context information (depending on dimensions)
+					$aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, $aArgs);
+					$iFieldSize = $oAttDef->GetMaxSize();
+					$iMaxComboLength = $oAttDef->GetMaximumComboLength();
+					if (count($aAllowedValues) >= $iMaxComboLength)
+					{
+						// too many choices, use an autocomplete
+						$oWidget = new UIAutoCompleteWidget($sAttCode, $sClass, $oAttDef->GetLabel(), $aAllowedValues, $value, $iId, $sNameSuffix, $sFieldPrefix);
+						$sHTMLValue = $oWidget->Display($oPage, $aArgs);
+						
+					}
+					else
+					{
+						// Few choices, use a normal 'select'
+						// In case there are no valid values, the select will be empty, thus blocking the user from validating the form
+						$sHTMLValue = "<select title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" id=\"$iId\">\n";
+						$sHTMLValue .= "<option value=\"\">".Dict::S('UI:SelectOne')."</option>\n";
+						foreach($aAllowedValues as $key => $display_value)
+						{
+							if ((count($aAllowedValues) == 1) && ($sMandatory == 'true') )
+							{
+								// When there is only once choice, select it by default
+								$sSelected = ' selected';
+							}
+							else
+							{
+								$sSelected = ($value == $key) ? ' selected' : '';
+							}
+							$sHTMLValue .= "<option value=\"$key\"$sSelected>$display_value</option>\n";
+						}
+						$sHTMLValue .= "</select>&nbsp;{$sValidationField}\n";
+					}
+					break;
+					
 				case 'String':
 				default:
 					$aEventsList[] ='validate';
@@ -1128,44 +1172,24 @@ EOF
 					$iFieldSize = $oAttDef->GetMaxSize();
 					if ($aAllowedValues !== null)
 					{
-						if (count($aAllowedValues) > 50)
+						// Discrete list of values, use a SELECT
+						$sHTMLValue = "<select title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" id=\"$iId\">\n";
+						$sHTMLValue .= "<option value=\"\">".Dict::S('UI:SelectOne')."</option>\n";
+						foreach($aAllowedValues as $key => $display_value)
 						{
-							// too many choices, use an autocomplete
-							// The input for the auto complete
-							if ($oAttDef->IsNull($value)) // Null values are displayed as ''
+							if ((count($aAllowedValues) == 1) && ($sMandatory == 'true') )
 							{
-								$sDisplayValue = '';
+								// When there is only once choice, select it by default
+								$sSelected = ' selected';
 							}
-							$sHTMLValue = "<input count=\"".count($aAllowedValues)."\" type=\"text\" id=\"label_$iId\" size=\"30\" maxlength=\"$iFieldSize\" value=\"$sDisplayValue\"/>&nbsp;{$sValidationField}";
-							// another hidden input to store & pass the object's Id
-							$sHTMLValue .= "<input type=\"hidden\" id=\"$iId\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" value=\"$value\" />\n";
-							$oPage->add_ready_script("\$('#label_$iId').autocomplete('./ajax.render.php', { scroll:true, minChars:3, formatItem:formatItem, autoFill:true, keyHolder:'#$iId', extraParams:{operation:'autocomplete', sclass:'$sClass',attCode:'".$sAttCode."'}});");
-							$oPage->add_ready_script("\$('#label_$iId').blur(function() { $(this).search(); } );");
-							$oPage->add_ready_script("\$('#label_$iId').result( function(event, data, formatted) { OnAutoComplete('$iId', event, data, formatted); } );");
-							$aEventsList[] ='change';
-						}
-						else
-						{
-							// Few choices, use a normal 'select'
-							// In case there are no valid values, the select will be empty, thus blocking the user from validating the form
-							$sHTMLValue = "<select title=\"$sHelpText\" name=\"attr_{$sFieldPrefix}{$sAttCode}{$sNameSuffix}\" id=\"$iId\">\n";
-							$sHTMLValue .= "<option value=\"\">".Dict::S('UI:SelectOne')."</option>\n";
-							foreach($aAllowedValues as $key => $display_value)
+							else
 							{
-								if ((count($aAllowedValues) == 1) && ($sMandatory == 'true') )
-								{
-									// When there is only once choice, select it by default
-									$sSelected = ' selected';
-								}
-								else
-								{
-									$sSelected = ($value == $key) ? ' selected' : '';
-								}
-								$sHTMLValue .= "<option value=\"$key\"$sSelected>$display_value</option>\n";
+								$sSelected = ($value == $key) ? ' selected' : '';
 							}
-							$sHTMLValue .= "</select>&nbsp;{$sValidationField}\n";
-							$aEventsList[] ='change';
+							$sHTMLValue .= "<option value=\"$key\"$sSelected>$display_value</option>\n";
 						}
+						$sHTMLValue .= "</select>&nbsp;{$sValidationField}\n";
+						$aEventsList[] ='change';
 					}
 					else
 					{
@@ -1173,7 +1197,7 @@ EOF
 						$aEventsList[] ='keyup';
 						$aEventsList[] ='change';
 					}
-					break;
+				break;
 			}
 			$sPattern = addslashes($oAttDef->GetValidationPattern()); //'^([0-9]+)$';
 			if (!empty($aEventsList))

+ 20 - 13
application/displayblock.class.inc.php

@@ -214,7 +214,7 @@ class DisplayBlock
 	{
 		$sHtml = '';
 		$aExtraParams = array_merge($aExtraParams, $this->m_aParams);
-		$aExtraParams['block_id'] = $sId;
+		$aExtraParams['currentId'] = $sId;
 		$sExtraParams = addslashes(str_replace('"', "'", json_encode($aExtraParams))); // JSON encode, change the style of the quotes and escape them
 		
 		$bAutoReload = false;
@@ -253,7 +253,7 @@ class DisplayBlock
 		{
 			// render now
 			$sHtml .= "<div id=\"$sId\" class=\"display_block\">\n";
-			$sHtml .= $this->GetRenderContent($oPage, $aExtraParams);
+			$sHtml .= $this->GetRenderContent($oPage, $aExtraParams, $sId);
 			$sHtml .= "</div>\n";
 		}
 		else
@@ -303,10 +303,18 @@ class DisplayBlock
 	
 	public function RenderContent(WebPage $oPage, $aExtraParams = array())
 	{
-		$oPage->add($this->GetRenderContent($oPage, $aExtraParams));
+		if (empty($aExtraParams['currentId']))
+		{
+			$sId = $oPage->GetUniqueId(); // Works only if the page is not an Ajax one !
+		}
+		else
+		{
+			$sId = $aExtraParams['currentId'];
+		}
+		$oPage->add($this->GetRenderContent($oPage, $aExtraParams, $sId));
 	}
 	
-	public function GetRenderContent(WebPage $oPage, $aExtraParams = array())
+	public function GetRenderContent(WebPage $oPage, $aExtraParams = array(), $sId)
 	{
 		$sHtml = '';
 		// Add the extra params into the filter if they make sense for such a filter
@@ -718,22 +726,21 @@ class DisplayBlock
 			break;
 			
 			case 'search':
-			static $iSearchSectionId = 1;
 			$sStyle = (isset($aExtraParams['open']) && ($aExtraParams['open'] == 'true')) ? 'SearchDrawer' : 'SearchDrawer DrawerClosed';
-			$sHtml .= "<div id=\"Search_$iSearchSectionId\" class=\"$sStyle\">\n";
+			$sHtml .= "<div id=\"ds_$sId\" class=\"$sStyle\">\n";
 			$oPage->add_ready_script(
 <<<EOF
-	$("#LnkSearch_$iSearchSectionId").click( function() {
-		$("#Search_$iSearchSectionId").slideToggle('normal', function() { $("#Search_$iSearchSectionId").parent().resize(); } );
-		$("#LnkSearch_$iSearchSectionId").toggleClass('open');
+	$("#dh_$sId").click( function() {
+		$("#ds_$sId").slideToggle('normal', function() { $("#ds_$sId").parent().resize(); } );
+		$("#dh_$sId").toggleClass('open');
 	});
 EOF
 			);
+			$aExtraParams['currentId'] = $sId;
 			$sHtml .= cmdbAbstractObject::GetSearchForm($oPage, $this->m_oSet, $aExtraParams);
 	 		$sHtml .= "</div>\n";
 	 		$sHtml .= "<div class=\"HRDrawer\"></div>\n";
-	 		$sHtml .= "<div id=\"LnkSearch_$iSearchSectionId\" class=\"DrawerHandle\">".Dict::S('UI:SearchToggle')."</div>\n";
-	 		$iSearchSectionId++;
+	 		$sHtml .= "<div id=\"dh_$sId\" class=\"DrawerHandle\">".Dict::S('UI:SearchToggle')."</div>\n";
 			break;
 			
 			case 'open_flash_chart':
@@ -868,7 +875,7 @@ EOF
  */
 class HistoryBlock extends DisplayBlock
 {
-	public function GetRenderContent(WebPage $oPage, $aExtraParams = array())
+	public function GetRenderContent(WebPage $oPage, $aExtraParams = array(), $sId)
 	{
 		$sHtml = '';
 		$oSet = new CMDBObjectSet($this->m_oFilter, array('date'=>false));
@@ -950,7 +957,7 @@ class MenuBlock extends DisplayBlock
 	 * an object in with the same tab active by default as the tab that was active when selecting
 	 * the "Modify..." action.
 	 */
-	public function GetRenderContent(WebPage $oPage, $aExtraParams = array())
+	public function GetRenderContent(WebPage $oPage, $aExtraParams = array(), $sId)
 	{
 		$sHtml = '';
 		$oAppContext = new ApplicationContext();

+ 185 - 0
application/ui.autocompletewidget.class.inc.php

@@ -0,0 +1,185 @@
+<?php
+// Copyright (C) 2010 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+/**
+ * Class UIAutoCompleteWidget
+ * UI wdiget for displaying and editing external keys when
+ * A simple drop-down list is not enough...
+ * 
+ * The layout is the following
+ * 
+ * +-- #label_<id> (input)-------+  +-----------+
+ * |                             |  | Browse... |
+ * +-----------------------------+  +-----------+
+ * 
+ * And the popup dialog has the following layout:
+ * 
+ * +------------------- ac_dlg_<id> (div)-----------+
+ * + +--- ds_<id> (div)---------------------------+ |
+ * | | +------------- fs_<id> (form)------------+ | |
+ * | | | +--------+---+                         | | |
+ * | | | | Class  | V |                         | | |
+ * | | | +--------+---+                         | | |
+ * | | |                                        | | |
+ * | | |    S e a r c h   F o r m               | | |
+ * | | |                           +--------+   | | |
+ * | | |                           | Search |   | | |
+ * | | |                           +--------+   | | |
+ * | | +----------------------------------------+ | |
+ * | +--------------+-dh_<id>-+--------------------+ |
+ * |                \ Search /                      |
+ * |                 +------+                       |
+ * | +--- fr_<id> (form)--------------------------+ |
+ * | | +------------ dr_<id> (div)--------------+ | |
+ * | | |                                        | | |
+ * | | |      S e a r c h  R e s u l t s        | | |
+ * | | |                                        | | |
+ * | | +----------------------------------------+ | |
+ * | |   +--------+    +-----+                    | |
+ * | |   | Cancel |    | Add |                    | |
+ * | |   +--------+    +-----+                    | |
+ * | +--------------------------------------------+ |
+ * +------------------------------------------------+
+ * @author      Erwan Taloc <erwan.taloc@combodo.com>
+ * @author      Romain Quetiez <romain.quetiez@combodo.com>
+ * @author      Denis Flaven <denis.flaven@combodo.com>
+ * @license     http://www.opensource.org/licenses/gpl-3.0.html LGPL
+ */
+
+require_once('../application/webpage.class.inc.php');
+require_once('../application/displayblock.class.inc.php');
+
+class UIAutoCompleteWidget 
+{
+	protected static $iWidgetIndex = 0;
+	protected $sAttCode;
+	protected $sNameSuffix;
+	protected $iId;
+	protected $sTitle;
+	
+	public function __construct($sAttCode, $sClass, $sTitle, $aAllowedValues, $value, $iInputId, $sNameSuffix = '', $sFieldPrefix = '')
+	{
+		self::$iWidgetIndex++;
+		$this->sAttCode = $sAttCode;
+		$this->sClass = $sClass;
+		$this->oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
+		$this->sNameSuffix = $sNameSuffix;
+		$this->iId = $iInputId;
+		$this->aAllowedValues = $aAllowedValues;
+		$this->value = $value;
+		$this->sFieldPrefix = $sFieldPrefix;
+		$this->sTargetClass = $this->oAttDef->GetTargetClass();
+		$this->sTitle = $sTitle;
+	}
+	
+	/**
+	 * Get the HTML fragment corresponding to the linkset editing widget
+	 * @param WebPage $oP The web page used for all the output
+	 * @param Hash $aArgs Extra context arguments
+	 * @return string The HTML fragment to be inserted into the page
+	 */
+	public function Display(WebPage $oPage, $aArgs = array())
+	{
+		if ($this->oAttDef->IsNull($this->value)) // Null values are displayed as ''
+		{
+			$sDisplayValue = '';
+		}
+		else
+		{
+			$sDisplayValue = $this->GetObjectName($this->value);
+		}
+		$sMessage = Dict::S('UI:Message:EmptyList:UseSearchForm');
+		$oPage->add_ready_script(
+<<<EOF
+	oACWidget_{$this->iId} = new AutocompleteWidget('$this->iId', '$this->sClass', '$this->sAttCode', '$this->sNameSuffix');
+	oACWidget_{$this->iId}.emptyHtml = "<div style=\"background: #fff; border:0; text-align:center; vertical-align:middle;\"><p>$sMessage</p></div>";
+EOF
+);
+		$iMinChars = $this->oAttDef->GetMinAutoCompleteChars();
+
+		// the input for the auto-complete
+		$sHTMLValue = "<input count=\"".count($this->aAllowedValues)."\" type=\"text\" id=\"label_$this->iId\" size=\"30\" maxlength=\"$iFieldSize\" value=\"$sDisplayValue\"/>&nbsp;<a class=\"no-arrow\" href=\"javascript:oACWidget_{$this->iId}.Search();\"><img style=\"border:0;vertical-align:middle;\" src=\"../images/mini_search.gif\" /></a>&nbsp;<span id=\"v_{$this->iId}\"></span>";
+
+		// another hidden input to store & pass the object's Id
+		$sHTMLValue .= "<input type=\"hidden\" id=\"$this->iId\" name=\"attr_{$this->sFieldPrefix}{$this->sAttCode}{$this->sNameSuffix}\" value=\"$this->value\" />\n";
+
+		// Scripts to start the autocomplete and bind some events to it
+		$oPage->add_ready_script("\$('#label_$this->iId').autocomplete('./ajax.render.php', { scroll:true, minChars:{$iMinChars}, formatItem:formatItem, autoFill:false, matchContains:true, keyHolder:'#{$this->iId}', extraParams:{operation:'autocomplete', sclass:'$this->sClass',attCode:'".$this->sAttCode."'}});");
+		$oPage->add_ready_script("\$('#label_$this->iId').blur(function() { $(this).search(); } );");
+		$oPage->add_ready_script("\$('#label_$this->iId').result( function(event, data, formatted) { OnAutoComplete('$this->iId', event, data, formatted); } );");
+		$oPage->add_ready_script("\$('#ac_dlg_$this->iId').dialog({ width: $(window).width()*0.8, height: $(window).height()*0.8, autoOpen: false, modal: true, title: '$this->sTitle', resizeStop: oACWidget_{$this->iId}.UpdateSizes, close: oACWidget_{$this->iId}.OnClose });\n");
+		$oPage->add_at_the_end($this->GetSearchDialog($oPage)); // To prevent adding forms inside the main form
+
+		return $sHTMLValue;
+	}
+	
+	protected function GetSearchDialog(WebPage $oPage)
+	{
+		$sHTML = '<div id="ac_dlg_'.$this->iId.'"><div class="wizContainer" style="vertical-align:top;"><div id="dc_'.$this->iId.'">';
+
+		$oFilter = new DBObjectSearch($this->sTargetClass);
+		$oSet = new CMDBObjectSet($oFilter);
+		$oBlock = new DisplayBlock($oFilter, 'search', false);
+		$sHTML .= $oBlock->GetDisplay($oPage, $this->iId, array('open' => true, 'currentId' => $this->iId));
+		$sHTML .= "<form id=\"fr_{$this->iId}\" OnSubmit=\"return oACWidget_{$this->iId}.DoOk();\">\n";
+		$sHTML .= "<div id=\"dr_{$this->iId}\" style=\"vertical-align:top;background: #fff;height:100%;overflow:auto;padding:0;border:0;\">\n";
+		$sHTML .= "<div style=\"background: #fff; border:0; text-align:center; vertical-align:middle;\"><p>".Dict::S('UI:Message:EmptyList:UseSearchForm')."</p></div>\n";
+		$sHTML .= "</div>\n";
+		$sHTML .= "<input type=\"button\" id=\"btn_cancel_{$this->iId}\" value=\"".Dict::S('UI:Button:Cancel')."\" onClick=\"$('#ac_dlg_{$this->iId}').dialog('close');\">&nbsp;&nbsp;";
+		$sHTML .= "<input type=\"button\" id=\"btn_ok_{$this->iId}\" value=\"".Dict::S('UI:Button:Ok')."\"  onClick=\"oACWidget_{$this->iId}.DoOk();\">";
+		$sHTML .= "</div>\n";
+		$sHTML .= "</form>\n";
+		$sHTML .= '</div></div></div>';
+
+		$oPage->add_ready_script("$('#fs_{$this->iId}').bind('submit.uilinksWizard', oACWidget_{$this->iId}.DoSearchObjects);");
+		$oPage->add_ready_script("$('#dc_{$this->iId}').resize(oACWidget_{$this->iId}.UpdateSizes);");
+
+		return $sHTML;
+	}
+
+	/**
+	 * Search for objects to be selected
+	 * @param WebPage $oP The page used for the output (usually an AjaxWebPage)
+	 * @param string $sRemoteClass Name of the "remote" class to perform the search on, must be a derived class of m_sRemoteClass
+	 * @param Array $aAlreadyLinkedIds List of IDs of objects of "remote" class already linked, to be filtered out of the search
+	 */
+	public function SearchObjectsToSelect(WebPage $oP, $sTargetClass = '')
+	{
+		if ($sTargetClass != '')
+		{
+			// assert(MetaModel::IsParentClass($this->m_sRemoteClass, $sRemoteClass));
+			$oFilter = new DBObjectSearch($sTargetClass);
+		}
+		else
+		{
+			// No remote class specified use the one defined in the linkedset
+			$oFilter = new DBObjectSearch($this->sTargetClass);		
+		}
+		$oFilter->AddCondition('id', array_keys($this->aAllowedValues), 'IN');
+		$oSet = new CMDBObjectSet($oFilter);
+		$oBlock = new DisplayBlock($oFilter, 'list', false);
+		$oBlock->Display($oP, $this->iId, array('menu' => false, 'selection_mode' => true, 'selection_type' => 'single', 'display_limit' => false)); // Don't display the 'Actions' menu on the results
+	}
+	
+	/**
+	 * Get the display name of the selected object, to fill back the autocomplete
+	 */
+	public function GetObjectName($iObjId)
+	{
+		$oObj = MetaModel::GetObject($this->sTargetClass, $iObjId);
+		return $oObj->GetName();
+	}
+}
+?>

+ 12 - 1
application/webpage.class.inc.php

@@ -45,6 +45,7 @@ class WebPage
     protected $a_include_stylesheets;
     protected $a_headers;
     protected $a_base;
+    protected $iNextId;
     
     public function __construct($s_title)
     {
@@ -57,6 +58,7 @@ class WebPage
         $this->a_linked_stylesheets = array();
         $this->a_headers = array();
         $this->a_base = array( 'href' => '', 'target' => '');
+        $this->iNextId = 0;
         ob_start(); // Start capturing the output
     }
 	
@@ -358,5 +360,14 @@ class WebPage
 		}
 		return $sTag;
 	}
+	
+	/**
+	 * Get an ID (for any kind of HTML tag) that is guaranteed unique in this page
+	 * @return int The unique ID (in this page)
+	 */
+	public function GetUniqueId()
+	{
+		return $this->iNextId++;
+	}
 }
-?>
+?>

+ 17 - 7
core/attributedef.class.inc.php

@@ -262,11 +262,11 @@ abstract class AttributeDefinition
 		return (string)$sValue;
 	}
 
-	public function GetAllowedValues($aArgs = array(), $sBeginsWith = '')
+	public function GetAllowedValues($aArgs = array(), $sContains = '')
 	{
 		$oValSetDef = $this->GetValuesDef();
 		if (!$oValSetDef) return null;
-		return $oValSetDef->GetValues($aArgs, $sBeginsWith);
+		return $oValSetDef->GetValues($aArgs, $sContains);
 	}
 	
 	/**
@@ -1295,9 +1295,9 @@ class AttributeEnum extends AttributeString
 		return $sLabel;
 	}
 
-	public function GetAllowedValues($aArgs = array(), $sBeginsWith = '')
+	public function GetAllowedValues($aArgs = array(), $sContains = '')
 	{
-		$aRawValues = parent::GetAllowedValues($aArgs, $sBeginsWith);
+		$aRawValues = parent::GetAllowedValues($aArgs, $sContains);
 		if (is_null($aRawValues)) return null;
 		$aLocalizedValues = array();
 		foreach ($aRawValues as $sKey => $sValue)
@@ -1729,17 +1729,17 @@ class AttributeExternalKey extends AttributeDBFieldVoid
 		return $oValSetDef;
 	}
 
-	public function GetAllowedValues($aArgs = array(), $sBeginsWith = '')
+	public function GetAllowedValues($aArgs = array(), $sContains = '')
 	{
 		try
 		{
-			return parent::GetAllowedValues($aArgs, $sBeginsWith);
+			return parent::GetAllowedValues($aArgs, $sContains);
 		}
 		catch (MissingQueryArgument $e)
 		{
 			// Some required arguments could not be found, enlarge to any existing value
 			$oValSetDef = new ValueSetObjects('SELECT '.$this->GetTargetClass());
-			return $oValSetDef->GetValues($aArgs, $sBeginsWith);
+			return $oValSetDef->GetValues($aArgs, $sContains);
 		}
 	}
 
@@ -1765,6 +1765,16 @@ class AttributeExternalKey extends AttributeDBFieldVoid
 		if (MetaModel::IsValidObject($proposedValue)) return $proposedValue->GetKey();
 		return (int)$proposedValue;
 	}
+	
+	public function GetMaximumComboLength()
+	{
+		return $this->GetOptional('max_combo_length', utils::GetConfig()->Get('max_combo_length'));
+	}
+	
+	public function GetMinAutoCompleteChars()
+	{
+		return $this->GetOptional('min_autocomplete_chars', utils::GetConfig()->Get('min_autocomplete_chars'));
+	}
 }
 
 /**

+ 16 - 0
core/config.class.inc.php

@@ -118,6 +118,22 @@ class Config
 			'source_of_value' => '',
 			'show_in_conf_sample' => true,
 		),
+		'max_combo_length' => array(
+			'type' => 'int',
+			'description' => 'The maximum number of elements in a drop-down list. If more then an autocomplete will be used',
+			'default' => 50,
+			'value' => 50,
+			'source_of_value' => '',
+			'show_in_conf_sample' => false,
+		),
+		'min_autocomplete_chars' => array(
+			'type' => 'int',
+			'description' => 'The minimum number of characters to type in order to trigger the "autocomplete" behavior',
+			'default' => 3,
+			'value' => 3,
+			'source_of_value' => '',
+			'show_in_conf_sample' => false,
+		),
 	);
 
 	public function IsProperty($sPropCode)

+ 2 - 2
core/filterdef.class.inc.php

@@ -198,10 +198,10 @@ class FilterFromAttribute extends FilterDefinition
 		return $oAttDef->GetValuesDef();
 	}
 
-	public function GetAllowedValues($aArgs = array(), $sBeginsWith = '')
+	public function GetAllowedValues($aArgs = array(), $sContains = '')
 	{
 		$oAttDef = $this->Get("refattribute");
-		return $oAttDef->GetAllowedValues($aArgs, $sBeginsWith);
+		return $oAttDef->GetAllowedValues($aArgs, $sContains);
 	}
 
 	public function GetOperators()

+ 4 - 4
core/metamodel.class.php

@@ -902,16 +902,16 @@ abstract class MetaModel
 	// Allowed values
 	//
 
-	public static function GetAllowedValues_att($sClass, $sAttCode, $aArgs = array(), $sBeginsWith = '')
+	public static function GetAllowedValues_att($sClass, $sAttCode, $aArgs = array(), $sContains = '')
 	{
 		$oAttDef = self::GetAttributeDef($sClass, $sAttCode);
-		return $oAttDef->GetAllowedValues($aArgs, $sBeginsWith);
+		return $oAttDef->GetAllowedValues($aArgs, $sContains);
 	}
 
-	public static function GetAllowedValues_flt($sClass, $sFltCode, $aArgs = array(), $sBeginsWith = '')
+	public static function GetAllowedValues_flt($sClass, $sFltCode, $aArgs = array(), $sContains = '')
 	{
 		$oFltDef = self::GetClassFilterDef($sClass, $sFltCode);
-		return $oFltDef->GetAllowedValues($aArgs, $sBeginsWith);
+		return $oFltDef->GetAllowedValues($aArgs, $sContains);
 	}
 
 	//

+ 3 - 5
core/valuesetdef.class.inc.php

@@ -51,25 +51,23 @@ abstract class ValueSetDefinition
 	}
 
 
-	public function GetValues($aArgs, $sBeginsWith = '')
+	public function GetValues($aArgs, $sContains = '')
 	{
 		if (!$this->m_bIsLoaded)
 		{
 			$this->LoadValues($aArgs);
 			$this->m_bIsLoaded = true;
 		}
-		if (strlen($sBeginsWith) == 0)
+		if (strlen($sContains) == 0)
 		{
 			$aRet = $this->m_aValues;
 		}
 		else
 		{
-			$iCheckedLen = strlen($sBeginsWith);
-			$sBeginsWith = strtolower($sBeginsWith);
 			$aRet = array();
 			foreach ($this->m_aValues as $sKey=>$sValue)
 			{
-				if (strtolower(substr($sValue, 0, $iCheckedLen)) == $sBeginsWith)
+				if (stripos($sValue, $sContains) !== false)
 				{
 					$aRet[$sKey] = $sValue;
 				}

+ 3 - 0
css/light-grey.css

@@ -886,4 +886,7 @@ tr.row_modified td {
 tr.row_added td {
 	border-bottom: 1px #ccc solid;
 	padding: 2px;
+}
+a.truncated {
+	cursor: pointer;
 }

binární
images/mini_search.gif


+ 177 - 0
js/autocompletewidget.js

@@ -0,0 +1,177 @@
+// Copyright (C) 2010 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+function AutocompleteWidget(id, sClass, sAttCode, sSuffix)
+{
+	this.id = id;
+	this.sClass = sClass;
+	this.sAttCode = sAttCode;
+	this.sSuffix = sSuffix;
+	this.emptyHtml = ''; // content to be displayed when the search results are empty (when opening the dialog) 
+	this.emptyOnClose = true; // Workaround for the JQuery dialog being very slow when opening and closing if the content contains many INPUT tags
+	var me = this;
+	this.Init = function()
+	{
+		// make sure that the form is clean
+		$('#linkedset_'+this.id+' .selection').each( function() { this.checked = false; });
+		$('#'+this.id+'_btnRemove').attr('disabled','disabled');
+		$('#'+this.id+'_linksToRemove').val('');
+	}
+	
+	this.Search = function()
+	{
+		$('#ac_dlg_'+me.id).dialog('open');
+		this.UpdateSizes();
+		this.UpdateButtons();
+	}
+	
+	this.UpdateSizes = function()
+	{
+		var dlg = $('#ac_dlg_'+me.id);
+		var searchForm = dlg.find('div.display_block:first'); // Top search form, enclosing display_block
+		var results = $('#dr_'+me.id);
+		padding_right = parseInt(dlg.css('padding-right').replace('px', ''));
+		padding_left = parseInt(dlg.css('padding-left').replace('px', ''));
+		padding_top = parseInt(dlg.css('padding-top').replace('px', ''));
+		padding_bottom = parseInt(dlg.css('padding-bottom').replace('px', ''));
+		width = dlg.innerWidth() - padding_right - padding_left - 22; // 5 (margin-left) + 5 (padding-left) + 5 (padding-right) + 5 (margin-right) + 2 for rounding !
+		height = dlg.innerHeight() - padding_top - padding_bottom -22;
+		form_height = searchForm.outerHeight();
+		results.height(height - form_height - 40); // Leave some space for the buttons
+	}
+	
+	
+	this.UpdateButtons = function()
+	{
+		var okBtn = $('#btn_ok_'+me.id);
+		if ($('#fr_'+me.id+' input[name=selectObject]:checked').length > 0)
+		{
+			okBtn.attr('disabled', '');
+		}
+		else
+		{
+			okBtn.attr('disabled', 'disabled');
+		}
+	}
+	
+	var ajax_request = null;
+	
+	this.DoSearchObjects = function(id)
+	{
+		var theMap = { sAttCode: me.sAttCode,
+					   iInputId: me.id,
+					   sSuffix: me.sSuffix,
+					 }
+		
+		// Gather the parameters from the search form
+		$('#fs_'+me.id+' :input').each(
+			function(i)
+			{
+				if (this.name != '')
+				{
+					theMap[this.name] = this.value;
+				}
+			}
+		);
+		
+		oWizardHelper.UpdateWizard();
+		theMap['json'] = oWizardHelper.ToJSON();
+		
+		theMap['sRemoteClass'] = theMap['class'];  // swap 'class' (defined in the form) and 'remoteClass'
+		theMap['class'] = me.sClass;
+		theMap.operation = 'searchObjectsToSelect'; // Override what is defined in the form itself
+		
+		sSearchAreaId = '#dr_'+me.id;
+		//$(sSearchAreaId).html('<div style="text-align:center;width:100%;height:24px;vertical-align:middle;"><img src="../images/indicator.gif" /></div>');
+		$(sSearchAreaId).block();
+		me.UpdateButtons();
+
+		// Make sure that we cancel any pending request before issuing another
+		// since responses may arrive in arbitrary order
+		if (ajax_request != null)
+		{
+			ajax_request.abort();
+			ajax_request = null;
+		}
+		
+		// Run the query and display the results
+		ajax_request = $.post( 'ajax.render.php', theMap, 
+			function(data)
+			{
+				$(sSearchAreaId).html(data);
+				$(sSearchAreaId+' .listResults').tableHover();
+				$(sSearchAreaId+' .listResults').tablesorter( { headers: {0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables
+				$('#fr_'+me.id+' input:radio').click(function() { me.UpdateButtons(); });
+				me.UpdateButtons();
+				ajax_request = null;
+			},
+			'html'
+		);
+
+		return false; // Don't submit the form, stay in the current page !
+	}
+	
+	this.DoOk = function()
+	{
+		var iObjectId = $('#fr_'+me.id+' input[name=selectObject]:checked').val();
+		$('#ac_dlg_'+this.id).dialog('close');
+		$('#label_'+this.id).addClass('ac_loading');
+
+		// Query the server again to get the display name of the selected object
+		var theMap = { sAttCode: me.sAttCode,
+				   iInputId: me.id,
+				   iObjectId: iObjectId,
+				   sSuffix: me.sSuffix,
+				   'class': me.sClass,
+				   operation: 'getObjectName'
+				 }
+	
+		// Make sure that we cancel any pending request before issuing another
+		// since responses may arrive in arbitrary order
+		if (ajax_request != null)
+		{
+			ajax_request.abort();
+			ajax_request = null;
+		}
+		
+		// Run the query and get the result back directly in JSON
+		ajax_request = $.post( 'ajax.render.php', theMap, 
+			function(data)
+			{
+				$('#label_'+me.id).val(data.name);
+				$('#label_'+me.id).removeClass('ac_loading');
+				$('#'+me.id).val(iObjectId);
+				$('#'+me.id).trigger('validate');
+				ajax_request = null;
+			},
+			'json'
+		);
+		
+		return false; // Do NOT submit the form in case we are called by OnSubmit...
+	}
+	
+	// Workaround for a ui.jquery limitation: if the content of
+	// the dialog contains many INPUTs, closing and opening the
+	// dialog is very slow. So empty it each time.
+	this.OnClose = function()
+	{
+		// called by the dialog, so in the context 'this' points to the jQueryObject
+		if (me.emptyOnClose)
+		{
+			$('#dr_'+me.id).html(me.emptyHtml);
+		}
+		$('#label_'+me.id).focus();
+	}
+}

+ 50 - 30
js/utils.js

@@ -1,35 +1,54 @@
 // Some general purpose JS functions for the iTop application
 /**
  * Reload a truncated list
- */ 
+ */
+aTruncatedLists = {}; // To keep track of the list being loaded, each member is an ajaxRequest object
+
 function ReloadTruncatedList(divId, sSerializedFilter, sExtraParams)
 {
-	$('#'+divId).addClass('loading');
+	$('#'+divId).block();
 	//$('#'+divId).blockUI();
-	$.post('ajax.render.php?style=list',
+	if (aTruncatedLists[divId] != undefined)
+	{
+		try
+		{
+			aAjaxRequest = aTruncatedLists[divId];
+			aAjaxRequest.abort();
+		}
+		catch(e)
+		{
+			// Do nothing special, just continue
+			console.log('Uh,uh, exception !');
+		}
+	}
+	aTruncatedLists[divId] = $.post('ajax.render.php?style=list',
 	   { operation: 'ajax', filter: sSerializedFilter, extra_params: sExtraParams },
-	   function(data){
-		 $('#'+divId).empty();
-		 $('#'+divId).append(data);
-		 $('#'+divId).removeClass('loading');
-		 $('#'+divId+' .listResults').tableHover(); // hover tables
-		 $('#'+divId+' .listResults').each( function()
-				{
-					var table = $(this);
-					var id = $(this).parent();
-					var checkbox = (table.find('th:first :checkbox').length > 0);
-					if (checkbox)
+	     function(data)
+	     {
+			 aTruncatedLists[divId] = undefined;
+			 if (data.length > 0)
+			 {
+				 $('#'+divId).html(data);
+				 $('#'+divId+' .listResults').tableHover(); // hover tables
+				 $('#'+divId+' .listResults').each( function()
 					{
-						// There is a checkbox in the first column, don't make it sortable
-						table.tablesorter( { headers: { 0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables
-					}
-					else
-					{
-						// There is NO checkbox in the first column, all columns are considered sortable
-						table.tablesorter( { widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables
-					}
-				});
-		 //$('#'+divId).unblockUI();
+						var table = $(this);
+						var id = $(this).parent();
+						aTruncatedLists[divId] = undefined;
+						var checkbox = (table.find('th:first :checkbox').length > 0);
+						if (checkbox)
+						{
+							// There is a checkbox in the first column, don't make it sortable
+							table.tablesorter( { headers: { 0: {sorter: false}}, widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables
+						}
+						else
+						{
+							// There is NO checkbox in the first column, all columns are considered sortable
+							table.tablesorter( { widgets: ['myZebra', 'truncatedList']} ); // sortable and zebra tables
+						}
+					});
+				 $('#'+divId).unblock();
+			 }
 		}
 	 );
 }
@@ -38,6 +57,7 @@ function ReloadTruncatedList(divId, sSerializedFilter, sExtraParams)
  */
 function TruncateList(divId, iLimit, sNewLabel, sLinkLabel)
 {
+	$('#'+divId).block();
 	var iCount = 0;
 	$('#'+divId+' table.listResults tr:gt('+iLimit+')').each( function(){
 			$(this).remove();
@@ -47,13 +67,14 @@ function TruncateList(divId, iLimit, sNewLabel, sLinkLabel)
 	$('#'+divId+' table.listResults').addClass('truncated');
 	$('#trc_'+divId).html(sLinkLabel);
 	$('#'+divId+' .listResults').trigger("update"); //  Reset the cache
+	$('#'+divId).unblock();
 }
 /**
  * Reload any block -- used for periodic auto-reload
  */ 
 function ReloadBlock(divId, sStyle, sSerializedFilter, sExtraParams)
 {
-	$('#'+divId).addClass('loading');
+	$('#'+divId).block();
 	//$('#'+divId).blockUI();
 	$.post('ajax.render.php?style='+sStyle,
 	   { operation: 'ajax', filter: sSerializedFilter, extra_params: sExtraParams },
@@ -101,11 +122,10 @@ function UpdateFileName(id, sNewFileName)
  */
 function ReloadSearchForm(divId, sClassName, sBaseClass, sContext)
 {
-    var oDiv = $('#'+divId);
+    var oDiv = $('#ds_'+divId);
 	oDiv.block();
-	var oFormEvents = $('#'+divId+' form').data('events');
-	var aSubmit = new Array();
-	
+	var oFormEvents = $('#ds_'+divId+' form').data('events');
+
 	// Save the submit handlers
     aSubmit = new Array();
 	if ( (oFormEvents != null) && (oFormEvents.submit != undefined))
@@ -123,7 +143,7 @@ function ReloadSearchForm(divId, sClassName, sBaseClass, sContext)
 		   oDiv.append(data);
 		   if (aSubmit.length > 0)
 		   {
-			    var oForm = $('#'+divId+' form'); // Form was reloaded, recompute it
+			    var oForm = $('#ds_'+divId+' form'); // Form was reloaded, recompute it
 				for(index = 0; index < aSubmit.length; index++)
 				{
 					// Restore the previously bound submit handlers

+ 5 - 0
pages/UI.php

@@ -729,6 +729,7 @@ try
 			$oP->add_linked_script("../js/wizardhelper.js");
 			$oP->add_linked_script("../js/wizard.utils.js");
 			$oP->add_linked_script("../js/linkswidget.js");
+			$oP->add_linked_script("../js/autocompletewidget.js");
 			$oP->add_linked_script("../js/jquery.blockUI.js");
 			$sClass = utils::ReadParam('class', '');
 			$sClassLabel = MetaModel::GetName($sClass);
@@ -778,6 +779,7 @@ try
 			$oP->add_linked_script("../js/wizardhelper.js");
 			$oP->add_linked_script("../js/wizard.utils.js");
 			$oP->add_linked_script("../js/linkswidget.js");
+			$oP->add_linked_script("../js/autocompletewidget.js");
 			$oP->add_linked_script("../js/jquery.blockUI.js");
 
 			$aArgs = utils::ReadParam('default', array());
@@ -941,6 +943,7 @@ try
 						$oP->add_linked_script("../js/wizardhelper.js");
 						$oP->add_linked_script("../js/wizard.utils.js");
 						$oP->add_linked_script("../js/linkswidget.js");
+						$oP->add_linked_script("../js/autocompletewidget.js");
 						$oP->add_linked_script("../js/jquery.blockUI.js");
 						$oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetName(), $sClassLabel));
 						$oP->add("<div class=\"page_header\">\n");
@@ -1077,6 +1080,7 @@ try
 				$oP->add_linked_script("../js/wizardhelper.js");
 				$oP->add_linked_script("../js/wizard.utils.js");
 				$oP->add_linked_script("../js/linkswidget.js");
+				$oP->add_linked_script("../js/autocompletewidget.js");
 				$oP->add_linked_script("../js/jquery.blockUI.js");
 				$oP->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel));
 				$oP->add("<h1>".MetaModel::GetClassIcon($sClass)."&nbsp;".Dict::Format('UI:CreationTitle_Class', $sClassLabel)."</h1>\n");
@@ -1152,6 +1156,7 @@ try
 			$oP->add_linked_script("../js/wizardhelper.js");
 			$oP->add_linked_script("../js/wizard.utils.js");
 			$oP->add_linked_script("../js/linkswidget.js");
+			$oP->add_linked_script("../js/autocompletewidget.js");
 			$oP->add_linked_script("../js/jquery.blockUI.js");
 			$oP->add("<div class=\"page_header\">\n");
 			$oP->add("<h1>$sActionLabel - <span class=\"hilite\">{$oObj->GetName()}</span></h1>\n");

+ 31 - 2
pages/ajax.render.php

@@ -28,6 +28,7 @@ require_once('../application/webpage.class.inc.php');
 require_once('../application/ajaxwebpage.class.inc.php');
 require_once('../application/wizardhelper.class.inc.php');
 require_once('../application/ui.linkswidget.class.inc.php');
+require_once('../application/ui.autocompletewidget.class.inc.php');
 
 require_once('../application/startup.inc.php');
 require_once('../application/user.preferences.class.inc.php');
@@ -68,6 +69,33 @@ switch($operation)
 	$oWidget->SearchObjectsToAdd($oPage, $sRemoteClass, $aAlreadyLinked);	
 	break;
 	
+	// ui.autocompletewidget
+	case 'searchObjectsToSelect':
+	$sTargetClass = utils::ReadParam('sRemoteClass', '');
+	$sAttCode = utils::ReadParam('sAttCode', '');
+	$iInputId = utils::ReadParam('iInputId', '');
+	$sSuffix = utils::ReadParam('sSuffix', '');
+	// To do: retrieve the object under construction & use it to filter the allowed values
+	$sJson = utils::ReadParam('json', '');
+	$oWizardHelper = WizardHelper::FromJSON($sJson);
+	$oObj = $oWizardHelper->GetTargetObject();
+	$aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, array('this' => $oObj));
+	$oWidget = new UIAutocompleteWidget($sAttCode, $sClass, '', $aAllowedValues, $oObj->Get($sAttCode), $iInputId, $sSuffix, '');
+	$oWidget->SearchObjectsToSelect($oPage, $sTargetClass);	
+	break;
+
+	// ui.autocompletewidget
+	case 'getObjectName':
+	$sTargetClass = utils::ReadParam('sTargetClass', '');
+	$sAttCode = utils::ReadParam('sAttCode', '');
+	$iInputId = utils::ReadParam('iInputId', '');
+	$iObjectId = utils::ReadParam('iObjectId', '');
+	$sSuffix = utils::ReadParam('sSuffix', '');
+	$oWidget = new UIAutocompleteWidget($sAttCode, $sClass, '', array(), '', $iInputId, $sSuffix, '');
+	$sName = $oWidget->GetObjectName($iObjectId);
+	echo json_encode(array('name' => $sName));	
+	break;
+	
 	// ui.linkswidget
 	case 'doAddObjects':
 	$sAttCode = utils::ReadParam('sAttCode', '');
@@ -78,7 +106,7 @@ switch($operation)
 	$oWidget = new UILinksWidget($sClass, $sAttCode, $iInputId, $sSuffix, $bDuplicates);
 	$oWidget->DoAddObjects($oPage, $aLinkedObjectIds);	
 	break;
-	
+		
 	case 'wizard_helper_preview':
 	$sJson = utils::ReadParam('json_obj', '');
 	$oWizardHelper = WizardHelper::FromJSON($sJson);
@@ -109,7 +137,8 @@ switch($operation)
 			$value = $oObj->Get($sAttCode);
 			$displayValue = $oObj->GetEditValue($sAttCode);
 			$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
-			$sHTMLValue = cmdbAbstractObject::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $value, $displayValue, $sId, '', 0, array('this' => $oObj));
+			$iFlags = MetaModel::GetAttributeFlags($sClass, $oObj->GetState(), $sAttCode);
+			$sHTMLValue = cmdbAbstractObject::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $value, $displayValue, $sId, '', $iFlags, array('this' => $oObj));
 			// Make sure that we immediatly validate the field when we reload it
 			$oPage->add_ready_script("$('#$sId').trigger('validate');");
 			$oWizardHelper->SetAllowedValuesHtml($sAttCode, $sHTMLValue);