Autocomplete Formtool

Overview


A formtool that provides autocomplete functionality.

Options

Attribute

Usage

ftList

As with the list formtool - a comma delimited list of value:label pairs. Mutually exclusive with the ftListData attribute.

ftListData

The name of a function that returns a string as described for ftList OR a query containing value and name columns. Mutually exclusive with the ftList attribute.

ftListDataTypename

The type that contains the ftListData method. Defaults to the type being edited.

ftDataInline

By default lists that have more than 1000 items are retrieved with ajax, and those with less are embedded inline with the HTML. Use this setting to override this behaviour.

ftCache

The length of time to keep the full list in memory. Useful for speeding up access to large datasets that don't change much. Defaults to 24h for ftList and not at all for ftListData.

ftCallbackLimit

The maximum number of items to return from ajax requests. Defaults to 10 items.

The Code

<cfcomponent extends="farcry.core.packages.formtools.field" name="autocomplete" displayname="autocomplete" hint="Field component to provide autocomplete functionality"> 
 
	<cfproperty name="ftList" required="false" default="" hint="comma separated list of values or variable:value pairs to appear in the drop down. e.g apple,orange,kiwi or APP:apple,ORA:orange,KIW:kiwi" />
	<cfproperty name="ftListData" required="false" default="" hint="Method call that must return a string in the same variable value pair format as the ftlist attribute OR a query containing the columns value & name. Method gets passed the objectid of the currently edited object as an argument. e.g apple,orange,kiwi or APP:apple,ORA:orange,KIW:kiwi or queryNew('value,name')" />
	<cfproperty name="ftListDataTypename" required="false" default="" hint="Specific typename to call ftlistdata method on." />
	<cfproperty name="ftDataInline" required="false" default="" hint="By default data is included inline for lists of less than 1000 items, and lists of more are handled through ajax requests. Set this attribute to override that behaviour." />
	<cfproperty name="ftCache" required="false" default="" hint="Cache list data for specified number of seconds. Default is to cache ftList data for 24 hours and ftListData not at all." />
	<cfproperty name="ftCallbackLimit" required="false" default="10" hint="The maximum number of items to return from ajax filtering" />
	<cfproperty name="ftClass" required="false" default="" hint="sets a class for the form element" />
	<cfproperty name="ftstyle" required="false" default="" hint="allows in line styles to be added to form element" />
	
	
	<cffunction name="init" access="public" returntype="any" output="false" hint="Returns a copy of this initialised object">
		
		<cfreturn this>
	</cffunction>
	
	<cffunction name="getListData" access="private" output="false" returntype="struct" hint="This will return a list that is used by the edit function">
		<cfargument name="objectid" required="false" type="string" default="" hint="The objectid of the record we are getting the list for if available.">
		<cfargument name="typename" required="true" type="string" hint="The name of the type that this property is part of.">
		<cfargument name="property" required="true" type="string" hint="The name of the property">
		<cfargument name="stPropMetadata" required="false" type="struct" default="#structnew()#" hint="The properties metadata if available">
		<cfargument name="filter" required="false" type="string" default="" hint="Filter results by this value. Note: only the label is filtered, not the value." />
		
		<cfset var rListData = "" />
		<cfset var oList = "" />
		<cfset var result = structnew() />
		<cfset var st = structnew() />
		<cfset var listitem = "" />
		<cfset var qLocalData = "" />
		<cfset var qTemp = "" />
		
		<cfset result.data = "" />
		<cfset result.current = structnew() />
		<cfset result.current.value = "" />
		<cfset result.current.label = "" />
		
		<cfif arguments.stPropMetadata.ftCache eq "">
			<cfif len(arguments.stPropMetadata.ftList)>
				<cfset arguments.stPropMetadata.ftCache = 86400 />
			<cfelse>
				<cfset arguments.stPropMetadata.ftCache = 0 />
			</cfif>
		</cfif>
		
		<cfif structIsEmpty(arguments.stPropMetadata)>
			<cfset arguments.stPropMetadata = application.fapi.getPropertyMetadata(typename="#arguments.typename#", property="#arguments.property#") />
		</cfif>
		
		<cfif arguments.stPropMetadata.ftCache gt 0 
			and structkeyexists(application.stCOAPI[arguments.typename].stProps[arguments.property],"cache")
			and application.stCOAPI[arguments.typename].stProps[arguments.property].cache.created gte dateadd("s",-arguments.stPropMetadata.ftCache,now())>
			
			<cfset qLocalData = application.stCOAPI[arguments.typename].stProps[arguments.property].cache.data />
			
		<cfelse>
		
			<cfif len(arguments.stPropMetadata.ftListData) >
				<cfif len(arguments.stPropMetadata.ftListDataTypename)>
					<cfset oList = application.fapi.getContentType(arguments.stPropMetadata.ftListDataTypename) />
				<cfelse>
					<cfset oList = application.fapi.getContentType(arguments.typename) />
				</cfif>
				
				<cfinvoke component="#oList#" method="#arguments.stPropMetadata.ftListData#" returnvariable="rListData">
					<cfinvokeargument name="objectid" value="#arguments.objectID#" />
				</cfinvoke>
				
				<cfif isQuery(rListData)>
					<cfif rListData.recordCount AND listFindNoCase(rListData.columnList, "value") AND listFindNoCase(rListData.columnList, "name")>
						<cfquery dbtype="query" name="qLocalData">select [value],[name] as label from rListData</cfquery>
					</cfif>
				<cfelse>
					<cfset qLocalData = querynew("value,label") />
					<cfloop list="#rListData#" index="listitem">
						<cfset queryaddrow(qLocalData) />
						<cfset querysetcell(qLocalData,"value",listfirst(listitem,":")) />
						<cfset querysetcell(qLocalData,"label",listlast(listitem,":")) />
					</cfloop>
				</cfif>
			<cfelse>
				<cfset qLocalData = querynew("value,label") />
				<cfloop list="#arguments.stPropMetadata.ftList#" index="listitem">
					<cfset queryaddrow(qLocalData) />
					<cfset querysetcell(qLocalData,"value",listfirst(listitem,":")) />
					<cfset querysetcell(qLocalData,"label",listlast(listitem,":")) />
				</cfloop>
			</cfif>
			
			<cfif arguments.stPropMetadata.ftCache gt 0>
				<cfset application.stCOAPI[arguments.typename].stProps[arguments.property].cache = structnew() />
				<cfset application.stCOAPI[arguments.typename].stProps[arguments.property].cache.data = qLocalData />
				<cfset application.stCOAPI[arguments.typename].stProps[arguments.property].cache.created = now() />
			</cfif>
			
		</cfif>
		
		<cfif arguments.stPropMetadata.ftDataInline eq "">
			<cfif qLocalData.recordcount lt 1000>
				<cfset arguments.stPropMetadata.ftDataInline = true />
			<cfelse>
				<cfset arguments.stPropMetadata.ftDataInline = false />
			</cfif>
		</cfif>
		
		<cfif not arguments.stPropMetadata.ftDataInline>
			<cfset arguments.stPropMetadata.ftCallbackLimit = -1 />
		</cfif>
		
		<cfquery dbtype="query" name="result.data" maxRows="#arguments.stPropMetadata.ftCallbackLimit#">
			select		*
			from		qLocalData
			<cfif len(arguments.filter)>
				where	lower(label) like <cfqueryparam cfsqltype="cf_sql_varchar" value="%#lcase(arguments.filter)#%" />
			</cfif>
		</cfquery>
		
		<cfif len(arguments.stPropMetadata.value)>
			<cfquery dbtype="query" name="qTemp">
				select * from qLocalData where [value]=<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.stPropMetadata.value#" />
			</cfquery>
			<cfif qTemp.recordcount>
				<cfset result.current.value = qTemp.value[1] />
				<cfset result.current.label = qTemp.label[1] />
			</cfif>
		</cfif>
			
		<cfreturn result />
	</cffunction>
	
	<cffunction name="serializeResult" access="public" output="false" returntype="string" hint="Converts result list to JSON">
		<cfargument name="q" type="query" required="true" />
		
		<cfset var json = "[ " />
		
		<cfloop query="arguments.q">
			<cfset json = json & '{ "value":"#arguments.q.value#", "label":"#arguments.q.label#" }' />
			<cfif arguments.q.currentrow lt arguments.q.recordcount><cfset json = json & ',' /></cfif>
		</cfloop>
		<cfset json = json & ' ]' />
		
		<cfreturn json />
	</cffunction>
	
	<cffunction name="edit" access="public" output="false" returntype="string" hint="his will return a string of formatted HTML text to enable the user to edit the data">
		<cfargument name="typename" required="true" type="string" hint="The name of the type that this field is part of.">
		<cfargument name="stObject" required="true" type="struct" hint="The object of the record that this field is part of.">
		<cfargument name="stMetadata" required="true" type="struct" hint="This is the metadata that is either setup as part of the type.cfc or overridden when calling ft:object by using the stMetadata argument.">
		<cfargument name="fieldname" required="true" type="string" hint="This is the name that will be used for the form field. It includes the prefix that will be used by ft:processform.">

		<cfset var html = "" />
		<cfset var optionValue = "" />
		<cfset var rListData = "" />
		<cfset var i = "" />
		<cfset var oList = "" />
		<cfset var stData = structnew() />

		<cfimport taglib="/farcry/core/tags/webskin" prefix="skin" />
		
		<cfset stData = getListData(objectid="#arguments.stobject.objectid#", 
									typename="#arguments.typename#",
									property="#arguments.stMetadata.name#",
									stPropMetadata="#arguments.stMetadata#") /> 
		
		<skin:loadJS id="jquery-ui" />
		<skin:loadCSS id="jquery-ui" />
		<skin:loadCSS id="jquery-autocomplete"><cfoutput>
			ul.ui-autocomplete { text-align: left; }
		</cfoutput></skin:loadCSS>
		<cfsavecontent variable="html"><cfoutput>
			<input type="text" id="#arguments.fieldname#label" name="#arguments.fieldname#label" class="textInput #arguments.stMetadata.ftClass#" style="#arguments.stMetadata.ftStyle#" value="#stData.current.label#">
			<input type="hidden" id="#arguments.fieldname#" name="#arguments.fieldname#" value="#stData.current.value#">
			<script type="text/javascript">
				$j( "###arguments.fieldname#label" ).autocomplete({
					minLength: 3,
					delay: 500,
					source: <cfif arguments.stMetadata.ftDataInline>#serializeResult(stData.data)#<cfelse>"#getAjaxURL(argumentCollection=arguments)#"</cfif>,
					focus: function( event, ui ) {
						$( "###arguments.fieldname#label" ).val( ui.item.label );
						$( "###arguments.fieldname#" ).val( ui.item.value );
						return false;
					},
					select: function( event, ui ) {
						$( "###arguments.fieldname#label" ).val( ui.item.label );
						$( "###arguments.fieldname#" ).val( ui.item.value );
						return false;
					}
				}).bind("keyup",function(){
					$j( "###arguments.fieldname#" ).val( this.value );
				}).data( "autocomplete" )._renderItem = function( ul, item ) {
					return $( "<li><a>"+item.label+"</a></li>" ).data( "item.autocomplete", item ).appendTo( ul );
				};
			</script>
		</cfoutput></cfsavecontent>
		
		<cfreturn html />
	</cffunction>
	
	
	<cffunction name="ajax" output="false" returntype="string" hint="Response to ajax requests for this formtool">
		<cfargument name="typename" required="true" type="string" hint="The name of the type that this field is part of.">
		<cfargument name="stObject" required="true" type="struct" hint="The object of the record that this field is part of.">
		<cfargument name="stMetadata" required="true" type="struct" hint="This is the metadata that is either setup as part of the type.cfc or overridden when calling ft:object by using the stMetadata argument.">
		<cfargument name="fieldname" required="true" type="string" hint="This is the name that will be used for the form field. It includes the prefix that will be used by ft:processform.">

		<cfset var stMD = duplicate(arguments.stMetadata) />
		<cfset var stData = "" />
		
		<cfset stMD.ajaxrequest = "true" />
		
		<cfparam name="url.term" />
		
		<cfset stData = getListData(objectid="#arguments.stobject.objectid#", 
									typename="#arguments.typename#",
									property="#arguments.stMetadata.name#",
									stPropMetadata="#arguments.stMetadata#",
									filter=url.term) />
		
		<cfreturn serializeResult(stData.data) />
	</cffunction>

	
	<cffunction name="display" access="public" output="true" returntype="string" hint="This will return a string of formatted HTML text to display.">
		<cfargument name="typename" required="true" type="string" hint="The name of the type that this field is part of.">
		<cfargument name="stObject" required="true" type="struct" hint="The object of the record that this field is part of.">
		<cfargument name="stMetadata" required="true" type="struct" hint="This is the metadata that is either setup as part of the type.cfc or overridden when calling ft:object by using the stMetadata argument.">
		<cfargument name="fieldname" required="true" type="string" hint="This is the name that will be used for the form field. It includes the prefix that will be used by ft:processform.">
		
		<cfset var stData = "" />
		
		<cfset stData = getListData(objectid="#arguments.stobject.objectid#", 
									typename="#arguments.typename#",
									property="#arguments.stMetadata.name#",
									stPropMetadata="#arguments.stMetadata#") /> 
		
		<cfreturn stData.current.label />
	</cffunction>
	
	
	<cffunction name="validate" access="public" output="true" returntype="struct" hint="This will return a struct with bSuccess and stError">
		<cfargument name="ObjectID" required="true" type="UUID" hint="The objectid of the object that this field is part of.">
		<cfargument name="Typename" required="true" type="string" hint="the typename of the objectid.">
		<cfargument name="stFieldPost" required="true" type="struct" hint="The fields that are relevent to this field type.">
		<cfargument name="stMetadata" required="true" type="struct" hint="This is the metadata that is either setup as part of the type.cfc or overridden when calling ft:object by using the stMetadata argument.">
		
		<cfset var stResult = structNew()>		
		<cfset stResult.bSuccess = true>
		<cfset stResult.value = arguments.stFieldPost.Value />
		<cfset stResult.stError = StructNew()>	

		<!--- --------------------------- --->
		<!--- Perform any validation here --->
		<!--- --------------------------- --->
		<cfif len(trim(stResult.value))>
		
			<!--- Remove any leading or trailing empty list items --->
			<cfif stResult.value EQ ",">
				<cfset stResult.value = "" />
			</cfif>
			<cfif left(stResult.value,1) EQ ",">
				<cfset stResult.value = right(stResult.value,len(stResult.value)-1) />
			</cfif>
			<cfif right(stResult.value,1) EQ ",">
				<cfset stResult.value = left(stResult.value,len(stResult.value)-1) />
			</cfif>			
		<cfelse>
			<cfset stResult.value = "" />
		</cfif>
					
		<!--- ----------------- --->
		<!--- Return the Result --->
		<!--- ----------------- --->
		<cfreturn stResult>
	</cffunction>
	
</cfcomponent>