//==============================================================================
// Copyright 2010-2026 Moraware, Inc.
//
// Purpose: Supports a search popup window for assisted selection list
// population. Allows you to type in the textbox and
// (asynchronously) bring up a selection list of available options.
//
//==============================================================================
/* eslint no-eval: "off" */
/*global
cancelEvent
checkResponseForError
ConsoleLogging
doPositionPicker
getObj
htmlEncode
jsrsExecuteWithErrorP
mjtElemData
rsCallbackHandleStandardJSONResponse
scrollElementIntoViewWithinContainerElement
setFocus
setFocusIfFocusable
StringificationUtils
stringStartsWith
trim
*/
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// eslint-disable-next-line no-unused-vars
var TypeAheadPopup = (function() {
var TYPEAHEAD_ITEM_TEXT_ALL = 'All',
TYPEAHEAD_ITEM_ID_ALL = -1,
// Whether a typeahead's text still names the value it is supposed to
// hold, for a caller about to commit that value.
//
// A typeahead field may be nullable, so empty text is a legitimate "no
// value". But where the value is id-backed, text that names nothing is
// an invalid state: the id field is cleared on every keystroke (see
// "pvClearForAndHandleTypeAheadChange()"), so text typed and never
// picked from the dropdown leaves the two disagreeing.
//
// Mutually exclusive, so a caller branches exactly once. Note that
// "ptrResolutionPending" is not a rejection: an answer is still coming,
// and putting an error in front of the user over a lookup that may yet
// succeed would be wrong.
//
// Returned today by "PagedPicker.resolvePendingTypeaheadText()",
// which answers it for a Paged Picker's own field. It lives here
// because the question is the typeahead's, not that control's: any
// typeahead carrying a hidden id field can be left in the same state.
PendingTypeaheadResolution_Enum = {
ptrNothingPending: 1,
ptrTextCleared: 2,
ptrUnresolvedText: 3,
ptrResolutionPending: 4
},
_test_suppressHideDropdown,
_typeAheadTextChangeTimer = null;
//--------------------------------------------------------------------------
//
// When debugging typeahead styling, it is extremely useful to be able to
// prevent the dropdown from hiding itself on blur, etc. This method allows
// you to suppress that behaviour.
//
//--------------------------------------------------------------------------
function pvSetSuppressHideDropdown(suppressHideDropdown_) {
_test_suppressHideDropdown = suppressHideDropdown_;
}
//--------------------------------------------------------------------------
// Reads the div's "isDropdownOpen" flag rather than its inline display:
// headers may emit the div with no inline style, and the renderers hide it
// for a tick before "doPositionPicker()" shows it.
//--------------------------------------------------------------------------
function pvIsTypeaheadDivShowing(objTypeAheadDiv_) {
return objTypeAheadDiv_.isDropdownOpen === true;
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pbIsTypeaheadDropdownShowing() {
var objTypeAheadDiv = getObj('typeAheadDiv'),
rcIsShowing =
!!objTypeAheadDiv &&
pvIsTypeaheadDivShowing(objTypeAheadDiv);
return rcIsShowing;
}
//--------------------------------------------------------------------------
//
// TODO: This method definitely doesn't belong in the TypeAheadPopup module.
// (DL: 2026-09-11)
//
//--------------------------------------------------------------------------
function pbSetInvalidTextClass(objTargetField_, isInvalid_) {
var strClassName = objTargetField_.className,
strInvalidClassName = 'invalidTextField';
if (isInvalid_) {
if (strClassName.indexOf(strInvalidClassName) < 0) {
strClassName = trim(strClassName + ' ' + strInvalidClassName);
}
} else {
strClassName = trim(strClassName.replace(strInvalidClassName, ''));
}
objTargetField_.className = strClassName;
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvUpdateTypeaheadTextboxValidation(params_) {
var objTypeAheadDiv = params_.typeaheadDiv,
objConfigParams = objTypeAheadDiv.typeAheadConfigParams,
tmpDoForceMatchItem =
objConfigParams && objConfigParams.forceMatchItem;
if(tmpDoForceMatchItem) {
// If the field doesn't match an item, mark it as invalid...
var theForm = getObj(objConfigParams.formId) || 0,
objTextField = theForm[objConfigParams.textFieldName] || 0,
objIdField = theForm[objConfigParams.idFieldName] || 0,
strTextFieldValue = objTextField.value || '';
if (objIdField.value || !strTextFieldValue.length) {
// Either there is a matching selection or the field is empty.
// Regardless, clear the invalid marker.
pbSetInvalidTextClass(
objTextField,
0); // isInvalid_
} else if (objTextField.form &&
objTextField.form.ignoreFieldValue &&
objTextField.form.ignoreFieldValue.value === strTextFieldValue) {
pbSetInvalidTextClass(
objTextField,
0); // isInvalid_
} else {
pbSetInvalidTextClass(
objTextField,
1); // isInvalid_
}
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pbHideTypeAheadDiv() {
var objTypeAheadDiv = getObj('typeAheadDiv'),
rc = 0;
if(objTypeAheadDiv) {
// Before hiding the typeahead div update the validation status if
// necessary.
pvUpdateTypeaheadTextboxValidation({
typeaheadDiv: objTypeAheadDiv
});
rc = pvIsTypeaheadDivShowing(objTypeAheadDiv);
if(!_test_suppressHideDropdown) {
objTypeAheadDiv.scrollTop = 0;
objTypeAheadDiv.ignoreBlur = false;
objTypeAheadDiv.typeAheadConfigParams = null;
objTypeAheadDiv.isDropdownOpen = false;
objTypeAheadDiv.innerHTML = '';
objTypeAheadDiv.style.display = 'none';
}
}
return rc;
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pbHideTypeAheadDivIfNotTypeAheadDiv(clickedElement_) {
var objTypeAheadDiv = getObj('typeAheadDiv'),
objTypeAheadConfigParams =
(objTypeAheadDiv || {}).typeAheadConfigParams;
if (objTypeAheadConfigParams) {
var doHideTypeAhead = true,
theForm = getObj(objTypeAheadConfigParams.formId),
objTextField =
(theForm || {})[objTypeAheadConfigParams.textFieldName];
if (objTextField) {
doHideTypeAhead = clickedElement_ !== objTextField;
}
if (doHideTypeAhead) {
pbHideTypeAheadDiv();
}
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvFireTypeAheadOnChangeIfNecessary(params_) {
var objTextControl = params_.textField,
tmpUseAsynchForOnChange = params_.useAsynchForOnChange,
strOnChangeFunction =
objTextControl.getAttribute('mjttypeaheadonchange');
if(strOnChangeFunction) {
// TODO: Work out a way to obviate these "eval()" calls.
if(tmpUseAsynchForOnChange) {
setTimeout(
function() {
eval(unescape(strOnChangeFunction));
},
0);
} else {
eval(unescape(strOnChangeFunction));
}
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvHandleExplicitTypeAheadSelection(params_) {
params_ = params_ || {};
var objResultOptions = params_.resultOptions,
rc = false,
objTypeAheadDiv = getObj('typeAheadDiv');
if (objTypeAheadDiv) {
var typeAheadConfigParams = objTypeAheadDiv.typeAheadConfigParams,
fnHandleExplicitSelection =
typeAheadConfigParams ?
StringificationUtils.getOrResolveFunction({
functionNameOrReference:
typeAheadConfigParams.fnHandleExplicitSelection,
descriptionOfFunctionBeingResolved:
'typeAheadConfigParams.fnHandleExplicitSelection',
suppressErrorsOnMissingNameOrReference: 1
}) :
null,
fnHandleExplicitSelectionP =
typeAheadConfigParams ?
StringificationUtils.getOrResolveFunction({
functionNameOrReference:
typeAheadConfigParams.fnHandleExplicitSelectionP,
descriptionOfFunctionBeingResolved:
'typeAheadConfigParams.fnHandleExplicitSelectionP',
suppressErrorsOnMissingNameOrReference: 1
}) :
null,
objExplicitSelectionResultOptions = {};
if(fnHandleExplicitSelection || fnHandleExplicitSelectionP) {
if(typeof objTypeAheadDiv.selectedRowIndex !== 'undefined' &&
objTypeAheadDiv.selectedRowIndex !== null &&
objTypeAheadDiv.selectedRowIndex > -1) {
var objSelectedRow = getObj('typeAheadRow' + objTypeAheadDiv.selectedRowIndex);
if(objSelectedRow) {
if(fnHandleExplicitSelectionP) {
fnHandleExplicitSelectionP({
idxSelectedRow: objTypeAheadDiv.selectedRowIndex,
selectedRow: objSelectedRow,
formId: typeAheadConfigParams.formId,
typeAheadConfigParams: typeAheadConfigParams,
explicitSelectionFnParameter: typeAheadConfigParams.explicitSelectionFnParameter,
arrTypeaheadRowVals: mjtElemData(objTypeAheadDiv, 'arrTypeaheadRowVals') || null,
explicitSelectionResultOptions: objExplicitSelectionResultOptions
});
if(objResultOptions) {
objResultOptions.suppressSetFocus =
objExplicitSelectionResultOptions.suppressSetFocus;
}
} else {
fnHandleExplicitSelection(
objTypeAheadDiv.selectedRowIndex,
objSelectedRow,
typeAheadConfigParams.formId,
typeAheadConfigParams,
typeAheadConfigParams.explicitSelectionFnParameter,
mjtElemData(objTypeAheadDiv, 'arrTypeaheadRowVals') || null);
}
rc = true;
} else {
if(typeAheadConfigParams.fnHandleExplicitEmptySelection) {
if(typeAheadConfigParams.fnHandleExplicitEmptySelection()) {
rc = true;
}
}
}
} else {
if(typeAheadConfigParams.fnHandleExplicitEmptySelection) {
if(typeAheadConfigParams.fnHandleExplicitEmptySelection()) {
rc = true;
}
}
}
}
}
return rc;
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pbScrollTypeaheadSelectionIntoViewIfNecessary() {
var objTypeAheadDiv = getObj('typeAheadDiv');
if (!objTypeAheadDiv.typeAheadConfigParams) {
// The type-ahead has already been disimissed.
return;
}
if (-1 === objTypeAheadDiv.selectedRowIndex) {
// No selection...no need to scroll...
return;
}
var objSelectedRow = getObj('typeAheadRow' + objTypeAheadDiv.selectedRowIndex);
if (objSelectedRow) {
scrollElementIntoViewWithinContainerElement(objSelectedRow, objTypeAheadDiv);
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvGetLastTypeaheadDisplayValue(objTextField_) {
return objTextField_.getAttribute('data-mjtLastTypeAheadDisplayValue');
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvSetLastTypeaheadDisplayValue(params_) {
var objTextField = params_.textField,
strDisplayValue = params_.displayValue,
tmpDontSetFieldValue = params_.dontSetFieldValue;
objTextField.setAttribute(
'data-mjtLastTypeAheadDisplayValue',
strDisplayValue);
if (!tmpDontSetFieldValue) {
objTextField.value = strDisplayValue;
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvCheckIfTypeaheadDisplayValueChangedAndSetLastValue(objTextField_) {
var strLastValue = pvGetLastTypeaheadDisplayValue(objTextField_),
strCurrValue = objTextField_.value,
rc = strLastValue !== strCurrValue;
if (rc) {
pvSetLastTypeaheadDisplayValue({
textField: objTextField_,
displayValue: strCurrValue,
dontSetFieldValue: 1
});
}
return rc;
}
//--------------------------------------------------------------------------
//
// Records a field's own typeahead configuration on the field itself, so
// that something holding nothing but the form and field name can find it
// later.
//
// The global "typeAheadDiv" is not that place. It carries whichever field
// was interacted with last, is cleared outright the moment the dropdown
// hides (see "hideTypeAheadDiv()"), and after a server round trip may well
// be describing an entirely different field by the time an answer comes
// back.
//
//--------------------------------------------------------------------------
function pvRememberTypeAheadConfigParamsForField(typeAheadConfigParams_) {
var theForm = getObj(typeAheadConfigParams_.formId),
objTextField =
theForm ? theForm[typeAheadConfigParams_.textFieldName] : null;
if (objTextField) {
mjtElemData(
objTextField,
'typeAheadConfigParams',
typeAheadConfigParams_);
}
}
//--------------------------------------------------------------------------
//
// The configuration last recorded against "objTextField_" by
// "pvRememberTypeAheadConfigParamsForField()", or null if that field has
// never been focused or typed into. Typing is what puts a field in the
// state this exists to answer for, so in practice a field with text to
// resolve has one.
//
//--------------------------------------------------------------------------
function pvGetTypeAheadConfigParamsForField(objTextField_) {
return objTextField_ ?
(mjtElemData(objTextField_, 'typeAheadConfigParams') || null) :
null;
}
//--------------------------------------------------------------------------
//
// Writes a chosen {id, displayValue} pair into the field pair a typeahead
// is built on: the visible text field and the hidden id field that backs
// it.
//
// Shared so that a value recovered by lookup lands in exactly the state a
// dropdown pick leaves behind, rather than in an approximation of it that
// drifts the first time either path changes. Deliberately takes the id and
// the display value themselves rather than a dropdown row, since the
// caller that needs this most has no row: see
// "pbResolveUnmatchedTypeaheadText()".
//
// Does not touch the dropdown, dismiss it, or move focus. Those belong to
// whoever is committing, and the two callers want opposite things of them.
//
//--------------------------------------------------------------------------
function pvCommitTypeAheadSelectionValue(params_) {
var objTypeAheadConfigParams = params_.typeAheadConfigParams,
strId = params_.id,
strDisplayValue = params_.displayValue,
objTypeAheadDiv = getObj('typeAheadDiv'),
theForm = getObj(objTypeAheadConfigParams.formId),
objTextField = theForm[objTypeAheadConfigParams.textFieldName],
objIdField = theForm[objTypeAheadConfigParams.idFieldName];
// Got a valid selection, so clear the invalid indicator.
pbSetInvalidTextClass(
objTextField,
0); // isInvalid_
// Be sure to ignore changes here.
// Don't wipe out the list simply because they've arrowed down...
if (objTypeAheadDiv) {
objTypeAheadDiv.ignoreTextChange = true;
}
if (_typeAheadTextChangeTimer) {
clearTimeout(_typeAheadTextChangeTimer);
}
pvSetLastTypeaheadDisplayValue({
textField: objTextField,
displayValue: strDisplayValue
});
objIdField.value = strId;
if (objTypeAheadDiv) {
objTypeAheadDiv.ignoreTextChange = false;
}
pvFireTypeAheadOnChangeIfNecessary({
textField: objTextField,
useAsynchForOnChange: false
});
}
//--------------------------------------------------------------------------
//
// TODO: Ideally, we'd simply be using element.classList to add and remove
// the style classes we need. However, to keep differences simpler
// until such time as we at least remove v1 support, this function
// will be used to set a class while exclusively preserving the new
// "typeAheadPriorSelection" style class and ignoring any others (as
// has always been done).
//
//--------------------------------------------------------------------------
function pvSetClassWhileRetainingPriorSelectionInd(params_) {
var objElement = params_.targetElement,
strNewClassName = params_.newClassName,
tmpWasMarkedPriorSelection;
if (objElement) {
tmpWasMarkedPriorSelection =
objElement.classList.contains('typeAheadPriorSelection');
objElement.className =
strNewClassName +
(tmpWasMarkedPriorSelection ? ' typeAheadPriorSelection' : '');
objElement.setAttribute('mjtclass', objElement.className);
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function ptSelectTypeAheadItem(params_) {
var tmpRowIndex = params_.rowIndex,
tmpActiveSelection = params_.activeSelection,
tmpSetFocus = params_.setFocus,
objEvent = params_.event,
rc = false,
objTypeAheadDiv = getObj('typeAheadDiv');
if(objEvent && objEvent.button && objEvent.button !== 1) {
return false;
}
if(!objTypeAheadDiv.typeAheadConfigParams) {
// Already possible duplicate select call. Regardless, there's no
// typeahead config, so there's nothing to do...
return false;
}
objTypeAheadDiv.ignoreBlur = tmpSetFocus ? true : false;
if(objTypeAheadDiv.selectedRowIndex !== tmpRowIndex) {
pvSetClassWhileRetainingPriorSelectionInd({
targetElement: getObj(
'typeAheadRow' + objTypeAheadDiv.selectedRowIndex),
newClassName: 'typeAheadRow'
});
}
objTypeAheadDiv.selectedRowIndex = tmpRowIndex;
var objSelectedRow = getObj('typeAheadRow' + tmpRowIndex),
objTypeaheadConfigParams = objTypeAheadDiv.typeAheadConfigParams,
strFormId = objTypeaheadConfigParams.formId,
strTextFieldName = objTypeaheadConfigParams.textFieldName,
tmpAsyncFocusOnSelect = objTypeaheadConfigParams.asyncFocusOnSelect,
theForm = getObj(strFormId),
objCurrText = theForm[strTextFieldName],
objExplicitSelectionResultOptions = {};
if(objSelectedRow) {
rc = true;
// Highlight the newly selected row...
pvSetClassWhileRetainingPriorSelectionInd({
targetElement: objSelectedRow,
newClassName: 'typeAheadSelectedRow'
});
pbScrollTypeaheadSelectionIntoViewIfNecessary();
// Now copy the value of the newly selected row into the textbox.
pvCommitTypeAheadSelectionValue({
typeAheadConfigParams: objTypeaheadConfigParams,
id: unescape(objSelectedRow.getAttribute('mjtid')),
displayValue: unescape(objSelectedRow.getAttribute('mjtvalue'))
});
if(tmpActiveSelection) {
pvHandleExplicitTypeAheadSelection({
resultOptions: objExplicitSelectionResultOptions
});
}
}
if(tmpSetFocus) {
if(!objExplicitSelectionResultOptions.suppressSetFocus) {
if(tmpAsyncFocusOnSelect) {
setTimeout(
function() {
setFocus(objCurrText);
},
0);
} else {
setFocus(objCurrText);
}
}
if(tmpActiveSelection) {
// Hide the type-ahead since we're actively selecting an item
// and the focus event might otherwise force it to re-display.
pbHideTypeAheadDiv();
}
}
return rc;
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function ptRemoveTypeAheadSelectionOrHoverHighlight(removeSelHightlight_) {
var i,
objRow,
strTargetClass =
removeSelHightlight_ ?
'typeAheadSelectedRow' :
'typeAheadHoverRow';
objRow = getObj('typeAheadRow0');
for(i = 0; objRow; ++i) {
if(objRow.className === strTargetClass) {
objRow.className = 'typeAheadRow';
if(!removeSelHightlight_) {
var objTypeAheadDiv = getObj('typeAheadDiv');
objTypeAheadDiv.selectedRowIndex = i;
}
}
objRow = getObj('typeAheadRow' + (i + 1));
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvMousedownOnTypeahead() {
var objTypeAheadDiv = getObj('typeAheadDiv');
objTypeAheadDiv.ignoreBlur = true;
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvBlurTypeahead() {
pbHideTypeAheadDiv();
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function ptSetFocusToTextField(params_) {
var strFormId = params_.formId,
strFieldName = params_.fieldName,
theForm = getObj(strFormId);
if(theForm) {
var objField = theForm[strFieldName];
if(objField) {
setFocus(objField);
}
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvClearSelection() {
var objTypeAheadDiv = getObj('typeAheadDiv');
if(objTypeAheadDiv) {
var objSelectedRow = getObj('typeAheadRow' + objTypeAheadDiv.selectedRowIndex);
if(objSelectedRow) {
objSelectedRow.className = 'typeAheadRow';
objTypeAheadDiv.selectedRowIndex = -1;
}
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvGetNoMatchFoundHtml(params_) {
var objTAConfig = params_.taConfig,
strAdditionalLastRowHtml = params_.additionalLastRowHtml,
objOriginalPayload = params_.originalPayload,
fnNoMessagePostfix = params_.fnNoMessagePostfix,
strNoDataMessageOverride = objTAConfig.noDataMessage,
strNoMessagePostfix =
fnNoMessagePostfix ?
fnNoMessagePostfix(
objTAConfig.showAdditionalLastRow,
objOriginalPayload) :
'',
strNoDataMessageToUse =
strNoDataMessageOverride ?
strNoDataMessageOverride + strNoMessagePostfix :
'No matches found',
strNoMatchFoundAdditionalClasses =
objTAConfig.typeAheadTableAdditionalClasses,
strClassAtr =
strNoMatchFoundAdditionalClasses ?
' class="' + strNoMatchFoundAdditionalClasses + '" ' :
'',
strNoMatchFoundHTML =
'
' +
'' +
'| ' + strNoDataMessageToUse + ' | ' +
'
';
if (objTAConfig) {
if(strAdditionalLastRowHtml && objTAConfig.showAdditionalLastRow) {
strNoMatchFoundHTML += strAdditionalLastRowHtml;
}
}
strNoMatchFoundHTML += '
';
return strNoMatchFoundHTML;
}
//--------------------------------------------------------------------------
//
// Readies the dropdown element for a fresh render and gathers everything
// both renderers need to know about the field being rendered for, or null
// when there is nothing to render into.
//
// Answers null in the two cases neither renderer can proceed from: the
// typeahead has already been dismissed (no config on the div), or the
// config names a form that is not there. Note the div is wired and reset
// before the form is looked up, so a missing form still leaves it in the
// state below; that ordering is preserved from the two copies this was
// lifted out of.
//
// The caller still owns the per-render state that changes as rows are
// emitted ("tmpIsExactMatch" and friends). Only what is fixed for the whole
// render belongs here.
//
//--------------------------------------------------------------------------
function pvPrepareTypeaheadRender() {
var objTypeAheadDiv = getObj('typeAheadDiv'),
objTAConfig = objTypeAheadDiv.typeAheadConfigParams,
rcRenderContext = null;
if (objTAConfig) {
// Make sure that clicking on the typeahead scrollbar doesn't cause
// the typeahead to be hidden and also make sure that once the
// typeahead loses focus, blur handling is turned back on again.
objTypeAheadDiv.onmousedown = pvMousedownOnTypeahead;
objTypeAheadDiv.onblur = pvBlurTypeahead;
objTypeAheadDiv.ignoreBlur = false;
objTypeAheadDiv.selectedRowIndex = -1;
var theForm = getObj(objTAConfig.formId);
if (theForm) {
var objCurrText = theForm[objTAConfig.textFieldName],
objCurrId = theForm[objTAConfig.idFieldName];
rcRenderContext = {
typeAheadDiv: objTypeAheadDiv,
taConfig: objTAConfig,
form: theForm,
currText: objCurrText,
currId: objCurrId,
originalIdValue: objCurrId ? objCurrId.value : null,
isText: objCurrText.value.length > 0,
doAutoMatch: objTAConfig.autoMatchOnKeyInput
};
} else {
// Not sure what may have gone wrong here. Don't bother
// continuing.
ConsoleLogging.logBlockClose(
'No Form! looking for formId=' + objTAConfig.formId);
}
}
return rcRenderContext;
}
//--------------------------------------------------------------------------
//
// Puts a finished dropdown on screen: shows the built markup, highlights
// the row an exact match picked out, and positions the whole thing.
//
// The counterpart to "pvPrepareTypeaheadRender()", and shared by both
// renderers for the same reason. Assembling the markup is the caller's
// job, because that is the half the two genuinely differ over; everything
// from there on is the same either way.
//
//
// (Named) Parameters:
//
// arrObjects: Only passed on to "fnPostProcessTypeaheadDisplay", and
// so only meaningful alongside it.
//
// fnPostProcessTypeaheadDisplay:
// Optional caller hook, invoked once the markup is in
// place, with "typeaheadDiv" and "arrObjects".
//
// html: The finished dropdown markup, rendered as-is.
//
// isExactMatch: Set when a row was recognized as naming exactly what is
// already typed. That row (named by the div's
// "selectedRowIndex", which the caller set as it emitted
// the row) is highlighted and the field's change hook
// fires. Ignored where the field does not auto-match.
//
// renderContext: What "pvPrepareTypeaheadRender()" answered for this
// render.
//
//--------------------------------------------------------------------------
function pvFinishTypeaheadRender(params_) {
var objRenderContext = params_.renderContext,
strHTML = params_.html,
tmpIsExactMatch = params_.isExactMatch,
fnPostProcessTypeaheadDisplay =
params_.fnPostProcessTypeaheadDisplay,
arrObjects = params_.arrObjects,
objTypeAheadDiv = objRenderContext.typeAheadDiv;
objTypeAheadDiv.style.display = 'none';
objTypeAheadDiv.isDropdownOpen = true;
objTypeAheadDiv.style.zIndex = 501;
objTypeAheadDiv.innerHTML = strHTML;
if (tmpIsExactMatch && objRenderContext.doAutoMatch) {
var objSelectedRow =
getObj('typeAheadRow' + objTypeAheadDiv.selectedRowIndex);
objSelectedRow.className = 'typeAheadSelectedRow';
objSelectedRow.setAttribute('mjtclass', 'typeAheadSelectedRow');
// We've found a match (and thus set the selected row and id value
// (if there is one) so fire the onchange event (if there is one)
pvFireTypeAheadOnChangeIfNecessary({
textField: objRenderContext.currText,
useAsynchForOnChange: false
});
}
if (fnPostProcessTypeaheadDisplay) {
fnPostProcessTypeaheadDisplay({
typeaheadDiv: objTypeAheadDiv,
arrObjects: arrObjects
});
}
setTimeout(doPositionPicker, 0);
}
//--------------------------------------------------------------------------
//
// Decides whether the row being emitted is the one the dropdown should open
// with already selected, and records it as such if so.
//
// Called once per row by both renderers, threading its verdict through
// "matchState" because the answer depends on the rows already seen: the
// rule is to take the FIRST row whose name is exactly what has been typed,
// unless a LATER row is an equally exact match and is also the value the
// field was already holding. That is why an exact match does not end the
// scan, and why "matchedOriginalIdValue" latches.
//
// Writes the matched row's id into the field's hidden id field as it goes,
// so a field left showing an auto-matched row is id-backed the same way one
// the user picked from is.
//
//
// (Named) Parameters:
//
// currentText: What the field currently holds, raw. A row matches when
// its name equals this case-insensitively.
//
// matchState: The scan's running verdict, carrying "isExactMatch" and
// "matchedOriginalIdValue", both of which this updates.
// The caller starts it off with both false and reads
// "isExactMatch" once every row has been offered.
//
// name: The row's display name, already unescaped.
//
// renderContext: What "pvPrepareTypeaheadRender()" answered for this
// render.
//
// rowId: Index of this row within the rendered dropdown, which
// is what gets recorded as the selected row. Not the
// index within the incoming data, which separator rows
// make differ.
//
// unescapedId: The row's id, already unescaped, since the two
// renderers get there differently.
//
//--------------------------------------------------------------------------
function pvApplyExactMatchIfRowMatches(params_) {
var objRenderContext = params_.renderContext,
objMatchState = params_.matchState,
strName = params_.name,
strUnescapedId = params_.unescapedId,
strCurrentText = params_.currentText,
tmpRowId = params_.rowId,
objCurrId = objRenderContext.currId;
if (!objMatchState.matchedOriginalIdValue &&
strName &&
strName.toLowerCase() === strCurrentText.toLowerCase()) {
// If there is more than one product with the same name, we want to
// select the first item with that name, unless the previously
// selected item is also an identical match, but is NOT the FIRST
// such match.
var tmpThisRowMatchesTheOriginalId =
objCurrId &&
(strUnescapedId === objRenderContext.originalIdValue);
if (objRenderContext.doAutoMatch &&
(tmpThisRowMatchesTheOriginalId ||
!objMatchState.isExactMatch)) {
// Either this row is an exact match and its id matches the
// previously selected id, or it is simply the first exact
// match.
//
// In the former case, we definitely want it to be the selected
// row (and definitely won't want any other row to be selected).
//
// In the latter case, we want this to be the selected row
// unless there is a later row that is also an exact match, and
// that is the same row as was previously selected.
if (objCurrId) {
objCurrId.value = strUnescapedId;
}
objMatchState.matchedOriginalIdValue =
objMatchState.matchedOriginalIdValue ||
tmpThisRowMatchesTheOriginalId;
objMatchState.isExactMatch = true;
objRenderContext.typeAheadDiv.selectedRowIndex = tmpRowId;
}
}
}
//--------------------------------------------------------------------------
// Whether one of a row's optional flag columns is set, which this row
// format spells as the string "1".
//
// An absent, null or negative index all mean the caller never supplied that
// column at all, which reads the same as the flag being off.
//--------------------------------------------------------------------------
function pvRowFlagIsSet(arrRowColumns_, idxFlagColumn_) {
return typeof idxFlagColumn_ !== 'undefined' &&
idxFlagColumn_ !== null &&
idxFlagColumn_ > -1 &&
'1' === arrRowColumns_[idxFlagColumn_];
}
//--------------------------------------------------------------------------
//
// Builds the dropdown's markup and puts it on screen. The one renderer
// behind both varieties of typeahead, which differ only in how a row
// arrives.
//
// The two reach a row's fields differently, by column index into a
// colon-delimited string or by property name on an object, and
// "fnDescribeRow" is the whole of that difference. Everything from the
// markup outwards is the same either way, which is why this exists rather
// than the two near-copies it replaced.
//
//
// (Named) Parameters:
//
// arrRows: The rows to render, in whatever shape "fnDescribeRow"
// understands. Empty renders the "no match found" row, or
// dismisses the dropdown outright when the field is also
// empty.
//
// fnDescribeRow: Called once per row, with that row and its index, to
// answer what this needs to know about it. Either
// {isSeparator: true} or an object carrying "rowVals"
// (stashed for the host's explicit-selection handler),
// "id"/"name" (unescaped), "escapedId"/"escapedName" (as
// they are to appear in the row's "mjtid"/"mjtvalue"
// attributes), "isInactive",
// "inactiveMeansComplete", "extraHTML" and
// "renderExtraOnTheLeft".
//
// hasExtraColumn: Whether rows carry a second column at all. Decides the
// colspan of the rows that span both, and whether an
// extra cell is emitted per row. Kept separate from any
// one row's "extraHTML" because the cell is emitted even
// for rows with nothing to put in it.
//
// showInactiveStateSuffix:
// Whether an inactive row's name is followed by
// "(Inactive)", or "(Complete)" where the row says its
// inactive state means completion. Only the JSON variety
// asks for it; the text variety's own entry point says
// why it deliberately does not.
//
// additionalFirstRowHtml,
// additionalLastRowHtml:
// Optional caller-supplied rows placed before and after
// the real ones. The last one is honoured only where the
// field's config asks for it, and is also what the "no
// match found" answer offers in place of any rows at all.
//
// areMoreItems: Set when the server truncated the result set, which
// adds a trailing "There are more items" row.
//
// forceFireTypeAheadOnChange:
// Fire the field's change hook even when the answer is
// empty and the dropdown is simply dismissed.
//
// forceIgnoreCurrentTextForBold:
// Render each name unemphasized rather than emphasizing
// the leading characters the user typed.
//
// arrObjects,
// fnPostProcessTypeaheadDisplay:
// Optional caller hook invoked once the markup is in
// place, and the rows handed to it. Passed straight
// through to "pvFinishTypeaheadRender()".
//
// fnNoMessagePostfix,
// originalPayload: Passed straight through to "pvGetNoMatchFoundHtml()",
// and so only consulted when nothing matched.
//
//--------------------------------------------------------------------------
function pvRenderTypeaheadDropdown(params_) {
var arrRows = params_.arrRows,
fnDescribeRow = params_.fnDescribeRow,
tmpHasExtraColumn = params_.hasExtraColumn,
tmpShowInactiveStateSuffix = params_.showInactiveStateSuffix,
tmpAreMoreItems = params_.areMoreItems,
tmpForceFireTypeAheadOnChange = params_.forceFireTypeAheadOnChange,
tmpForceIgnoreCurrentTextForBold =
params_.forceIgnoreCurrentTextForBold,
strAdditionalFirstRowHtml = params_.additionalFirstRowHtml,
strAdditionalLastRowHtml = params_.additionalLastRowHtml,
objRenderContext = pvPrepareTypeaheadRender();
if (!objRenderContext) {
// The type-ahead has already been dismissed, or its form is gone.
return;
}
var objTypeAheadDiv = objRenderContext.typeAheadDiv,
objTAConfig = objRenderContext.taConfig,
theForm = objRenderContext.form,
objCurrText = objRenderContext.currText,
objMatchState = {
isExactMatch: false,
matchedOriginalIdValue: false
};
if (!(arrRows && arrRows.length)) {
if (objRenderContext.isText) {
var strNoMatchFoundHtml =
pvGetNoMatchFoundHtml({
taConfig: objTAConfig,
additionalLastRowHtml: strAdditionalLastRowHtml,
originalPayload: params_.originalPayload,
fnNoMessagePostfix: params_.fnNoMessagePostfix
});
objTypeAheadDiv.style.display = 'none';
objTypeAheadDiv.isDropdownOpen = true;
objTypeAheadDiv.style.zIndex = 501;
objTypeAheadDiv.innerHTML = strNoMatchFoundHtml;
setTimeout(doPositionPicker, 0);
} else {
mjtElemData(objTypeAheadDiv, 'arrTypeaheadRowVals', null);
pbHideTypeAheadDiv();
if (tmpForceFireTypeAheadOnChange) {
pvFireTypeAheadOnChangeIfNecessary({
textField: objCurrText,
useAsynchForOnChange: false
});
}
}
} else {
pbSetInvalidTextClass(
objCurrText,
0); // isInvalid_
var strColspanAttr = tmpHasExtraColumn ? ' colspan="2"' : '',
strAdditionalClasses =
objTAConfig ?
(objTAConfig.typeAheadTableAdditionalClasses || '') :
'',
strHTML =
'' +
'',
currTextRaw = objCurrText.value,
currText =
(tmpForceIgnoreCurrentTextForBold ||
objTAConfig.ignoreCurrentTextForBold) ?
'' :
currTextRaw,
strExistingSelectionText = objTAConfig.existingSelectionText,
rowId = 0,
i,
arrTypeaheadRowVals = [];
objTypeAheadDiv.itemCount = arrRows.length;
mjtElemData(
objTypeAheadDiv,
'arrTypeaheadRowVals',
arrTypeaheadRowVals);
if (strAdditionalFirstRowHtml) {
strHTML += strAdditionalFirstRowHtml;
}
for (i = 0; i < arrRows.length; ++i) {
var objRowInfo = fnDescribeRow(arrRows[i], i),
strOnMouseDown =
'onmousedown="' +
'TypeAheadPopup.PROTECTED.selectTypeAheadItem({' +
'rowIndex: ' + rowId + ',' +
'activeSelection: true,' +
'setFocus: true,' +
'event: event})" ',
strOnMouseUp =
'onmouseup="' +
'TypeAheadPopup.hideTypeAheadDiv();' +
'TypeAheadPopup.PROTECTED.setFocusToTextField({' +
'formId: \'' + theForm.id + '\',' +
'fieldName: \'' + objCurrText.name + '\'});"';
if (objRowInfo.isSeparator) {
// It's a separator!
--objTypeAheadDiv.itemCount;
strHTML +=
'' +
' | ' +
'
';
} else {
arrTypeaheadRowVals[rowId] = objRowInfo.rowVals;
var strName = objRowInfo.name;
pvApplyExactMatchIfRowMatches({
renderContext: objRenderContext,
matchState: objMatchState,
name: strName,
unescapedId: objRowInfo.id,
currentText: currTextRaw,
rowId: rowId
});
var tmpIsInactive = objRowInfo.isInactive,
tmpRenderExtraOnTheLeft =
objRowInfo.renderExtraOnTheLeft,
strBolded = strName.substring(0, currText.length),
strPostBolded = strName.substring(currText.length),
tmpMarkAsPriorSelection =
strExistingSelectionText &&
strName &&
(strName.toLowerCase() ===
strExistingSelectionText.toLowerCase()),
strAdditionalRowClass =
tmpMarkAsPriorSelection ?
' typeAheadPriorSelection' :
'',
// Emitted for every row once the field has a second
// column at all, empty or not, so that the rows stay
// aligned with one another.
strExtraHTML =
tmpHasExtraColumn ?
'' +
(objRowInfo.extraHTML || '') +
' | ' :
'',
strRow =
'';
if (strExtraHTML && tmpRenderExtraOnTheLeft) {
strRow += strExtraHTML;
}
strRow +=
'| ';
if (tmpIsInactive) {
strRow +=
'';
}
strRow +=
'' +
htmlEncode(strBolded) +
'' +
htmlEncode(strPostBolded);
if (tmpIsInactive) {
if (tmpShowInactiveStateSuffix) {
strRow +=
' (' +
(objRowInfo.inactiveMeansComplete ?
'Complete' :
'Inactive') +
')';
}
strRow +=
'';
}
strRow +=
' | ';
if (strExtraHTML && !tmpRenderExtraOnTheLeft) {
strRow += strExtraHTML;
}
strRow +=
'
';
strHTML += strRow;
++rowId;
}
}
if (tmpAreMoreItems) {
var strMoreMsg =
' There are more items' +
(currText ?
' starting with "' + htmlEncode(currText) + '"' :
'');
strHTML +=
'' +
'| ' +
strMoreMsg +
' | ' +
'
';
}
if (objTAConfig) {
if (strAdditionalLastRowHtml &&
objTAConfig.showAdditionalLastRow) {
strHTML += strAdditionalLastRowHtml;
}
}
strHTML +=
'' +
'
';
pvFinishTypeaheadRender({
renderContext: objRenderContext,
html: strHTML,
isExactMatch: objMatchState.isExactMatch,
fnPostProcessTypeaheadDisplay:
params_.fnPostProcessTypeaheadDisplay,
arrObjects: params_.arrObjects
});
}
}
//--------------------------------------------------------------------------
//
// Renders the dropdown from rows delivered as escaped, colon-delimited
// strings, which is what the older remote-scripting responses answer with
// (see "pbRSCallbackGetTypeAheadSearchText()").
//
// The sibling of "pbProcessTypeAheadSearchOfJSONArray()", which does the
// same job for rows delivered as JSON objects. The two differ only in how a
// row's fields are reached: by column index here, by property name there.
//
//
// (Named) Parameters:
//
// arrEscapedValues: The rows to render, each one a single escaped,
// colon-delimited string whose fields are addressed by
// the column indexes below. An empty array renders the
// "no match found" row, or dismisses the dropdown
// outright when the field is also empty.
//
// idColumnIndex,
// nameColumnIndex:
// Which colon-delimited field of a row holds its id and
// its display name. Both required; everything else about
// a row is optional.
//
// extraHTMLColumnIndex:
// Optional field holding supplemental HTML to render in
// a second column beside the name. Absent, negative or
// null all mean "no second column", and the rendered
// rows carry no colspan.
//
// isInactiveColumnIndex:
// Optional field holding "1" for a row that should
// render as inactive.
//
// inactiveMeansCompleteColumnIndex:
// Optional field holding "1" where a row's inactive
// state means "complete" rather than plain inactive.
// Read here but not yet acted on, so it currently
// changes nothing about what is rendered.
//
// areMoreItems: Set when the server truncated the result set, which
// adds a trailing "There are more items" row. It is a
// message only; nothing fetches the rest.
//
// forceFireTypeAheadOnChange:
// Fire the field's own change hook even when the answer
// is empty and the dropdown is simply dismissed. Only
// matters on that path, since a rendered exact match
// fires it regardless.
//
// forceIgnoreCurrentTextForBold:
// Render each name unemphasized rather than emphasizing
// the leading characters the user typed. Also settable
// for the field as a whole, as the typeahead config's
// "ignoreCurrentTextForBold"; either one turns the
// emphasis off.
//
//--------------------------------------------------------------------------
function pvProcessTypeAheadSearchTextArray(params_) {
var idxIdColumn = params_.idColumnIndex,
idxNameColumn = params_.nameColumnIndex,
idxExtraHTMLColumn = params_.extraHTMLColumnIndex,
idxIsInactiveColumn = params_.isInactiveColumnIndex,
idxInactiveMeansCompleteColumn =
params_.inactiveMeansCompleteColumnIndex;
if (typeof idxExtraHTMLColumn === 'undefined' ||
null === idxExtraHTMLColumn ||
idxExtraHTMLColumn < 0) {
idxExtraHTMLColumn = -1;
}
pvRenderTypeaheadDropdown({
arrRows: params_.arrEscapedValues,
hasExtraColumn: idxExtraHTMLColumn > -1,
areMoreItems: params_.areMoreItems,
forceFireTypeAheadOnChange: params_.forceFireTypeAheadOnChange,
forceIgnoreCurrentTextForBold:
params_.forceIgnoreCurrentTextForBold,
// Deliberately off, and not an oversight waiting to be corrected.
// Callers of this variety commonly already say that a row is
// inactive in their own way, through the extra HTML column or by
// styling the whole row, so a suffix added here would say it a
// second time and could contradict what the caller is already
// showing.
//
// Should a caller ever want it, this is where it becomes that
// caller's own option rather than a blanket switch for the whole
// variety.
showInactiveStateSuffix: false,
fnDescribeRow: function (strRowValue_) {
var rcRowInfo;
if (strRowValue_ === '-') {
rcRowInfo = {
isSeparator: true
};
} else {
var arrRowColumns = strRowValue_.split(':'),
strEscapedId = arrRowColumns[idxIdColumn],
strEscapedName = arrRowColumns[idxNameColumn];
rcRowInfo = {
// Handed to the host's explicit-selection handler as
// "arrTypeaheadRowVals", still as the split row rather
// than anything this reshaped.
rowVals: arrRowColumns,
id: unescape(strEscapedId),
name: unescape(strEscapedName),
// Already escaped by whoever built the row, so they go
// into the attributes exactly as they arrived.
escapedId: strEscapedId,
escapedName: strEscapedName,
isInactive:
pvRowFlagIsSet(
arrRowColumns,
idxIsInactiveColumn),
inactiveMeansComplete:
pvRowFlagIsSet(
arrRowColumns,
idxInactiveMeansCompleteColumn),
extraHTML:
idxExtraHTMLColumn > -1 &&
idxExtraHTMLColumn < arrRowColumns.length ?
unescape(arrRowColumns[idxExtraHTMLColumn]) :
''
};
}
return rcRowInfo;
}
});
}
//--------------------------------------------------------------------------
// Processes Remote Scripting responses.
//--------------------------------------------------------------------------
function pbRSCallbackGetTypeAheadSearchText(responseString_, contextId_, processResultParams_) {
var objTypeAheadDiv = getObj('typeAheadDiv'),
objTAConfig = objTypeAheadDiv.typeAheadConfigParams;
if(!objTAConfig) {
// The type-ahead has already been dismissed.
return;
}
// At this point we know that a type-ahead is currently open, but we
// can't be sure that:
//
// 1) This callback is for this type-ahead (i.e. it could be a
// really slow response from a prior type-ahead)
// 2) This callback is the latest request from this type-ahead
//
// To handle these cases:
//
// First check that the remote scripting context id stored on the
// type-ahead element matches up with the context id of this callback.
//
// If it doesn't match, then this is at the very least a stale request.
// ContextIds look like mjtrs#, ex: mjtrs1
if(objTAConfig.expectedContextId &&
contextId_ &&
objTAConfig.expectedContextId !== contextId_) {
ConsoleLogging.logMessage(
'TypeAheadPopup: expected to receive callback from ContextId=' +
objTAConfig.expectedContextId +
', but instead received callback from ContextId=' + contextId_);
} else if(checkResponseForError(responseString_)) {
var arrEscapedValues,
tmpAreMoreItems;
if (!trim(responseString_)) {
// No items...process an empty array
arrEscapedValues = [];
} else {
// The response should be prefixed by a "1;" or a "0;",
// indicating whether or not there are more after what is
// listed...
tmpAreMoreItems = responseString_.substring(0, 1) === '1';
responseString_ = responseString_.substring(2);
arrEscapedValues = responseString_.split(',');
}
var tmpHasExtraColumnIndex =
typeof processResultParams_.extraHTMLColumnIndex !== 'undefined' &&
processResultParams_.extraHTMLColumnIndex !== null,
idxExtraHTMLColumn =
tmpHasExtraColumnIndex ?
processResultParams_.extraHTMLColumnIndex :
-1,
tmpHasIsInactiveColumnIndex =
typeof processResultParams_.isInactiveColumnIndex !== 'undefined' &&
processResultParams_.isInactiveColumnIndex !== null,
tmpIsInactiveColumnIndex =
tmpHasIsInactiveColumnIndex ?
processResultParams_.isInactiveColumnIndex :
-1,
tmpHasInactiveMeansCompleteColumnIndex =
typeof processResultParams_.inactiveMeansCompleteColumnIndex !== 'undefined' &&
processResultParams_.inactiveMeansCompleteColumnIndex !== null,
tmpInactiveMeansCompleteColumnIndex =
tmpHasInactiveMeansCompleteColumnIndex ?
processResultParams_.inactiveMeansCompleteColumnIndex :
-1;
pvProcessTypeAheadSearchTextArray({
arrEscapedValues: arrEscapedValues,
idColumnIndex: 0,
nameColumnIndex: 1,
extraHTMLColumnIndex: idxExtraHTMLColumn,
isInactiveColumnIndex: tmpIsInactiveColumnIndex,
inactiveMeansCompleteColumnIndex:
tmpInactiveMeansCompleteColumnIndex,
areMoreItems: tmpAreMoreItems,
forceFireTypeAheadOnChange: false,
forceIgnoreCurrentTextForBold:
processResultParams_.forceIgnoreCurrentTextForBold ? 1 : 0
});
}
}
//--------------------------------------------------------------------------
//
// params_:
// {
// extraHTMLPropertyName,
// renderExtraOnTheLeftPropertyName,
// arrObjects,
// forceFireTypeAheadOnChange,
// fnPostProcessTypeaheadDisplay
// }
//
//--------------------------------------------------------------------------
function pbProcessTypeAheadSearchOfJSONArray(params_) {
var strExtraHTMLPropertyName = params_.extraHTMLPropertyName,
strRenderExtraOnTheLeftPropertyName =
params_.renderExtraOnTheLeftPropertyName;
pvRenderTypeaheadDropdown({
arrRows: params_.arrObjects,
hasExtraColumn: !!strExtraHTMLPropertyName,
areMoreItems: params_.areMoreItems,
forceFireTypeAheadOnChange: params_.forceFireTypeAheadOnChange,
forceIgnoreCurrentTextForBold:
params_.forceIgnoreCurrentTextForBold,
additionalFirstRowHtml: params_.additionalFirstRowHtml,
additionalLastRowHtml: params_.additionalLastRowHtml,
originalPayload: params_.originalPayload,
fnNoMessagePostfix: params_.fnNoMessagePostfix,
fnPostProcessTypeaheadDisplay:
params_.fnPostProcessTypeaheadDisplay,
arrObjects: params_.arrObjects,
showInactiveStateSuffix: true,
fnDescribeRow: function (objRow_) {
var rcRowInfo;
if (objRow_.isSeparator) {
rcRowInfo = {
isSeparator: true
};
} else {
var strUnescapedId = objRow_.id,
strName = objRow_.name;
if (objRow_ &&
!(strUnescapedId || strName) &&
strUnescapedId !== '' &&
strName !== '') {
// Looks like the given rows are not objects, so just
// use them directly as both the id and name...
strUnescapedId = objRow_;
strName = objRow_;
}
rcRowInfo = {
// Handed to the host's explicit-selection handler as
// "arrTypeaheadRowVals", still as the row object the
// caller supplied.
rowVals: objRow_,
id: strUnescapedId,
name: strName,
// Unescaped as they arrive, so they are escaped here on
// the way into the row's attributes.
escapedId: escape(strUnescapedId),
escapedName: escape(strName),
isInactive: objRow_.isInactive,
inactiveMeansComplete: objRow_.inactiveMeansComplete,
extraHTML:
strExtraHTMLPropertyName ?
objRow_[strExtraHTMLPropertyName] || '' :
'',
renderExtraOnTheLeft:
strRenderExtraOnTheLeftPropertyName ?
objRow_[strRenderExtraOnTheLeftPropertyName] :
false
};
}
return rcRowInfo;
}
});
}
//--------------------------------------------------------------------------
//
// Processes callback data that has a JSON form.
//
//--------------------------------------------------------------------------
function pbRSCallbackGetTypeAheadSearchTextJSON(json_, optionsHolder_) {
var objOriginalOptions = optionsHolder_.originalOptions,
objOriginalPayload = optionsHolder_.originalPayload,
tmpContextId = optionsHolder_.contextId,
objTypeAheadDiv = getObj('typeAheadDiv'),
objTAConfig = (objTypeAheadDiv || {}).typeAheadConfigParams,
strArrayPropertyName =
objOriginalOptions.arrayPropertyName ?
objOriginalOptions.arrayPropertyName :
'array',
arrObjects = json_[strArrayPropertyName],
fnPreprocessArray = objOriginalOptions.fnPreprocessArray,
fnPreprocessObject = objOriginalOptions.fnPreprocessObject,
strSearchTerm = (objOriginalPayload || {}).term || '',
tmpAreMoreItems = json_.isMore;
if(!objTAConfig) {
// The type-ahead has already been dismissed.
return;
}
// At this point we know that a type-ahead is currently open, but we
// can't be sure that:
//
// 1) This callback is for this type-ahead (i.e. it could be a
// really slow response from a prior type-ahead)
// 2) This callback is the latest request from this type-ahead
//
// To handle these cases:
//
// First check that the remote scripting context id stored on the
// type-ahead element matches up with the context id of this callback.
//
// If it doesn't match, then this is at the very least a stale request.
// ContextIds look like mjtrs#, ex: mjtrs1
if(objTAConfig.expectedContextId &&
tmpContextId &&
objTAConfig.expectedContextId !== tmpContextId) {
ConsoleLogging.logMessage(
'TypeAheadPopup: expected to receive json-callback from ContextId=' +
objTAConfig.expectedContextId +
', but instead received callback from ContextId=' + tmpContextId);
return;
}
if(fnPreprocessArray) {
fnPreprocessArray({
arrItems: arrObjects,
searchTerm: strSearchTerm
});
}
if(fnPreprocessObject && arrObjects) {
arrObjects.forEach(
function(object_) {
fnPreprocessObject(object_);
});
}
pbProcessTypeAheadSearchOfJSONArray({
extraHTMLPropertyName: objOriginalOptions.extraHTMLPropertyName,
renderExtraOnTheLeftPropertyName: objOriginalOptions.renderExtraOnTheLeftPropertyName,
forceIgnoreCurrentTextForBold: objOriginalOptions.forceIgnoreCurrentTextForBold,
arrObjects: arrObjects,
areMoreItems: tmpAreMoreItems,
fnPostProcessTypeaheadDisplay: objOriginalOptions.fnPostProcessTypeaheadDisplay,
additionalLastRowHtml: objOriginalOptions.additionalLastRowHtml,
additionalFirstRowHtml: objOriginalOptions.additionalFirstRowHtml,
originalPayload: optionsHolder_.originalPayload,
fnNoMessagePostfix: objOriginalOptions.fnNoMessagePostfix
});
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pbUpdateTypeAheadBoxTimerFired() {
var objTypeAheadDiv = getObj('typeAheadDiv'),
configParams = objTypeAheadDiv.typeAheadConfigParams;
if(configParams) {
var theForm = getObj(configParams.formId);
// Do a lookup using the current text...
if(!theForm[configParams.textFieldName]) {
// Debugging info!
ConsoleLogging.logMessage(
'\n' +
'\n' +
'!!!!\n' +
'Can not find text field for type-ahead.\n' +
' configParams.textFieldName="' + configParams.textFieldName + '"\n' +
'!!!!');
}
var currText = theForm[configParams.textFieldName].value.toLowerCase(),
fnGetTypeAheadData;
if(currText.length === 0 && !configParams.showTypeAheadWhenUnmatched) {
// No items, which on an empty field means "dismiss the
// dropdown". The column indexes still have to be given, but
// nothing reads them with no row to address.
pvProcessTypeAheadSearchTextArray({
arrEscapedValues: [],
idColumnIndex: 0,
nameColumnIndex: 1,
areMoreItems: false,
forceFireTypeAheadOnChange: true
});
} else {
fnGetTypeAheadData =
StringificationUtils.getOrResolveFunction({
functionNameOrReference: configParams.fnGetTypeAheadData,
descriptionOfFunctionBeingResolved:
'configParams.fnGetTypeAheadData'
});
if(fnGetTypeAheadData) {
fnGetTypeAheadData(
theForm,
currText,
configParams,
configParams.getTypeAheadDataFnParameter);
}
}
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pbHandleTypeAheadTextBoxBlur() {
var objTypeAheadDiv = getObj('typeAheadDiv');
if(objTypeAheadDiv && !objTypeAheadDiv.ignoreBlur) {
pbHideTypeAheadDiv();
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvFireAsynchHandleTypeAheadTextChange() {
if(_typeAheadTextChangeTimer) {
clearTimeout(_typeAheadTextChangeTimer);
}
// By default, wait 1/3 of a second before running off for data...
var timeout = 300;
if(getObj('typeAheadDiv').typeAheadConfigParams.forceImmediatePopup) {
timeout = 0;
}
_typeAheadTextChangeTimer =
setTimeout(pbUpdateTypeAheadBoxTimerFired, timeout);
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pbHandleTypeAheadTextBoxFocus(e_, formId_, typeAheadConfigParams_) {
var objTypeAheadDiv = getObj('typeAheadDiv');
objTypeAheadDiv.ignoreBlur = false;
if(!objTypeAheadDiv.ignoreFocus) {
objTypeAheadDiv.typeAheadConfigParams = typeAheadConfigParams_;
pvRememberTypeAheadConfigParamsForField(typeAheadConfigParams_);
var theForm = getObj(typeAheadConfigParams_.formId),
objIdField = theForm[typeAheadConfigParams_.idFieldName];
if(typeAheadConfigParams_.forceTypeAheadOnFocus ||
(typeAheadConfigParams_.showTypeAheadWhenUnmatched && !objIdField.value.length)) {
pvFireAsynchHandleTypeAheadTextChange();
}
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvIncrementTypeAheadSelectionIndex(moveDown_) {
var objTypeAheadDiv = getObj('typeAheadDiv'),
itemCount = objTypeAheadDiv.itemCount ? objTypeAheadDiv.itemCount : 0;
ptRemoveTypeAheadSelectionOrHoverHighlight(false);
if(typeof objTypeAheadDiv.selectedRowIndex === 'undefined') {
// Nothing selected yet, just select the first one.
ptSelectTypeAheadItem({
rowIndex: 0,
activeSelection: false
});
} else {
var currSelIndex = parseInt(objTypeAheadDiv.selectedRowIndex, 10);
if(moveDown_) {
currSelIndex++;
} else {
--currSelIndex;
}
if(currSelIndex >= itemCount) {
currSelIndex = itemCount - 1;
}
if(currSelIndex < 0) {
currSelIndex = 0;
}
ptSelectTypeAheadItem({
rowIndex: currSelIndex,
activeSelection: false
});
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvSelectFirstLastInTypeahead(params_) {
var objTypeAheadDiv = params_.typeAheadDiv,
tmpFirst = params_.first;
if(pvIsTypeaheadDivShowing(objTypeAheadDiv)) {
var itemCount = objTypeAheadDiv.itemCount ? objTypeAheadDiv.itemCount : 0;
if(itemCount) {
ptSelectTypeAheadItem({
rowIndex: tmpFirst ? 0 : itemCount - 1,
activeSelection: false,
setFocus: false
});
}
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvPageTypeAheadIfNecessary(params_) {
var objTypeAheadDiv = params_.typeAheadDiv,
tmpUp = params_.up;
if(pvIsTypeaheadDivShowing(objTypeAheadDiv)) {
var tmpItemCount = objTypeAheadDiv.itemCount ? objTypeAheadDiv.itemCount : 0,
tmpSelectedIndex =
objTypeAheadDiv.selectedRowIndex ?
objTypeAheadDiv.selectedRowIndex :
0,
firstVisibleItem = 0,
lastVisibleItem = tmpItemCount - 1,
topOfViewport = objTypeAheadDiv.scrollTop,
bottomOfViewport = topOfViewport + objTypeAheadDiv.clientHeight - 1,
i;
for(i = 0; i < tmpItemCount; ++i) {
var objRow = getObj('typeAheadRow' + i);
if(topOfViewport + objRow.offsetHeight / 2 >= objRow.offsetTop &&
topOfViewport + objRow.offsetHeight / 2 < objRow.offsetTop + objRow.offsetHeight) {
// Found the first visible item...
firstVisibleItem = i;
}
if(bottomOfViewport - objRow.offsetHeight / 2 >= objRow.offsetTop &&
bottomOfViewport - objRow.offsetHeight / 2 < objRow.offsetTop + objRow.offsetHeight) {
// Found the last visible item...
lastVisibleItem = i;
}
}
var indexToSelect;
if(tmpUp && tmpSelectedIndex > firstVisibleItem) {
// Paging up, but we're not at the top of the viewport,
// so simply select the top item in the viewport.
indexToSelect = firstVisibleItem;
} else if((!tmpUp) && tmpSelectedIndex < lastVisibleItem) {
// Paging down, but we're not at the bottom of the viewport,
// so simply select the bottom item in the viewport.
indexToSelect = lastVisibleItem;
} else {
var itemsPerPage = lastVisibleItem - firstVisibleItem;
if(itemsPerPage < 1) {
itemsPerPage = 1;
}
if(tmpUp) {
// Paging to the top of the previous page...
indexToSelect = firstVisibleItem - itemsPerPage;
} else {
// Paging to the bottom of the next page...
indexToSelect = lastVisibleItem + itemsPerPage;
}
}
if(indexToSelect >= tmpItemCount) {
indexToSelect = tmpItemCount - 1;
}
if(indexToSelect < 0) {
indexToSelect = 0;
}
ptSelectTypeAheadItem({
rowIndex: indexToSelect,
activeSelection: false,
setFocus: false
});
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvClearForAndHandleTypeAheadChange(params_) {
var objIdField = params_.idField,
objTextField = params_.textField,
tmpUseAsynchForOnChange = params_.useAsynchForOnChange;
// Clear out the id pending selection...
if(objIdField && objIdField.value.length) {
objIdField.value = '';
pvFireTypeAheadOnChangeIfNecessary({
textField: objTextField,
useAsynchForOnChange: tmpUseAsynchForOnChange
});
}
pvFireAsynchHandleTypeAheadTextChange();
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pbHandleTypeAheadTextBoxOnInput(e_, formId_, typeAheadConfigParams_) {
var objTypeAheadDiv = getObj('typeAheadDiv');
if(!objTypeAheadDiv.ignoreTextChange) {
objTypeAheadDiv.typeAheadConfigParams = typeAheadConfigParams_;
pvRememberTypeAheadConfigParamsForField(typeAheadConfigParams_);
var theForm = getObj(typeAheadConfigParams_.formId),
objTextField = theForm[typeAheadConfigParams_.textFieldName],
objIdField = theForm[typeAheadConfigParams_.idFieldName];
if(pvCheckIfTypeaheadDisplayValueChangedAndSetLastValue(objTextField)) {
pvClearForAndHandleTypeAheadChange({
idField: objIdField,
textField: objTextField,
useAsynchForOnChange: false
});
}
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pbHandleTypeAheadTextBoxKeyDown(
e_,
formId_,
typeAheadConfigParams_,
handlingEnter_,
skipHideDialogOnEnter_) {
var objTypeAheadDiv = getObj('typeAheadDiv');
if(!objTypeAheadDiv.ignoreTextChange) {
objTypeAheadDiv.typeAheadConfigParams = typeAheadConfigParams_;
pvRememberTypeAheadConfigParamsForField(typeAheadConfigParams_);
var theForm = getObj(typeAheadConfigParams_.formId),
objTextField = theForm[typeAheadConfigParams_.textFieldName],
objIdField = theForm[typeAheadConfigParams_.idFieldName];
switch(e_.keyCode) {
case 38:
// up
pvIncrementTypeAheadSelectionIndex(false);
break;
case 40:
// down
if(e_.altKey) {
var strPickerActivation = objTextField.getAttribute('mjttypeaheadactivation');
if(strPickerActivation) {
pbHideTypeAheadDiv();
// TODO: Work out a way to obviate these "eval()"
// calls.
eval(strPickerActivation);
}
} else {
pvIncrementTypeAheadSelectionIndex(true);
}
break;
case 33:
// pageup
pvPageTypeAheadIfNecessary({
typeAheadDiv: objTypeAheadDiv,
up: true
});
return false;
case 34:
// pagedown
pvPageTypeAheadIfNecessary({
typeAheadDiv: objTypeAheadDiv,
up: false
});
return false;
case 27:
// Escape
if(pbHideTypeAheadDiv()) {
// The dropdown was showing, so prevent the event from
// propagating so that the only impact will be dismissal
// of the typeahead. (e.g. if this is in a dialog, the
// dialog won't also be dismissed.)
cancelEvent(e_);
return false;
}
break;
case 13:
if(pvHandleExplicitTypeAheadSelection()) {
return false;
} else {
if(!skipHideDialogOnEnter_) {
pbHideTypeAheadDiv();
}
if(!handlingEnter_) {
// Not handling enter separately, so return false
// to prevent the beep on IE.
return false;
}
}
break;
case 9: // Tab
break;
case 116: // F5
break;
case 37: // Left
break;
case 39: // Right
break;
case 16: // Shift
break;
case 36: // Home
if(e_.ctrlKey) {
pvSelectFirstLastInTypeahead({
typeAheadDiv: objTypeAheadDiv,
first: true
});
}
break;
case 35: // End
if(e_.ctrlKey) {
pvSelectFirstLastInTypeahead({
typeAheadDiv: objTypeAheadDiv,
first: false
});
}
break;
case 17: // Ctrl
break;
case 18: // Alt
break;
default:
if(e_.altKey) {
break;
}
if(e_.ctrlKey && (65 === e_.keyCode || 82 === e_.keyCode)) {
// Ctrl-A/Ctrl-R ... ignore these...
break;
}
// Whatever key the user hit may invalidate the current
// selection so clear it now (before we fetch new data) so
// that if the user hits 'enter' before the new data arrives
// they aren't taken to any previously selected item which
// may not match the last keys they entered.
pvClearSelection();
pvClearForAndHandleTypeAheadChange({
idField: objIdField,
textField: objTextField,
// Trigger the onchange event async because it is coming
// from a text change that has not yet completed.
// If a synchronous event is fired, then the textbox
// would still have stale text.
useAsynchForOnChange: true
});
break;
}
}
}
//--------------------------------------------------------------------------
//
// Used as the "fnHandleExplicitSelection" parameter when you want to
// support submitting a dialog on enter from a typeahead field, but you
// don't need to take any sort of specific custom action when a typeahead
// option is explicitly selected from the list by clicking or pressing
// enter.
//
// This function will simply dismiss the typeahead and try to set focus back
// to the underlying text field.
//
// This is used to support the behaviour of hiding the typeahead dropdown if
// is pressed while the dropdown is showing and an item is selected,
// and of allowing the dialog to submit if the dropdown is NOT showing with
// an item selected. (In order to actually see this behaviour, you need to
// also set the "handlingEnter_" flag of the
// "pbHandleTypeAheadTextBoxKeyDown()" function.)
//
// Note: The typeahead framework will pass the following parameters beyond
// the "typeAheadConfigParams_" parameter, but this helper doesn't
// need them, so they aren't included in its declared signature:
// explicitSelectionFnParameter_
// and arrTypeaheadRowVals_
//
//--------------------------------------------------------------------------
function pbSimpleExplicitSelectionHandler(
selectedRowIndex_,
objSelectedRow_,
formId_,
typeAheadConfigParams_) {
var theForm = getObj(formId_),
objTextField = theForm ? theForm[typeAheadConfigParams_.textFieldName] : 0;
pbHideTypeAheadDiv();
if(objTextField) {
// Asynchronously set focus back to the text field...
setTimeout(
function() {
setFocusIfFocusable(objTextField);
},
0);
}
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvSearchTermPermitsAllItem(searchTerm_) {
var tmpSearchTermLen = searchTerm_.length,
tmpAllLen = TYPEAHEAD_ITEM_TEXT_ALL.length,
rc = 0;
if (tmpSearchTermLen <= tmpAllLen) {
// Don't even bother with a text comparison unless the length of the
// search term is no longer than the text we're vetting. (If the
// search term is longer than the value we're filtering, there's no
// way it could be a match)
if (!tmpSearchTermLen) {
// Since there is no search term to filter values, the "All"
// item is definitely not being filtered out.
rc = 1;
} else {
rc =
stringStartsWith(
TYPEAHEAD_ITEM_TEXT_ALL, // string_
searchTerm_, // prefix_
1); // ignoreCase_
}
}
return rc;
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pbPrependAllItemToArray(params_) {
var arrItems = params_.arrItems,
strSearchTerm = params_.searchTerm;
if (arrItems && pvSearchTermPermitsAllItem(strSearchTerm)) {
var objAllItem = {
id: TYPEAHEAD_ITEM_ID_ALL,
name: TYPEAHEAD_ITEM_TEXT_ALL
};
arrItems.unshift(objAllItem);
}
return arrItems;
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pbBuildStringifiedTypeaheadInfo(params_) {
var objStringifiableParams = params_.stringifiableParams,
rcStringified = JSON.stringify(objStringifiableParams);
return rcStringified;
}
//--------------------------------------------------------------------------
//
// The one option out of "arrCandidates_" whose name is the text that was
// typed, or null when none of them is.
//
// Compared trimmed and case-insensitively, the same way a typed value is
// judged against an existing selection in the first place, so that the
// answer to "does this text name a real option" does not depend on which
// of the two asked. "Exact" is deliberately the client's definition and
// not the server's: a lookup that decided for itself what counted as a
// match could hand back something the user never typed.
//
//--------------------------------------------------------------------------
function pvFindExactMatch(arrCandidates_, searchText_) {
var strWantedName = searchText_.trim().toLowerCase(),
rcMatch = null;
arrCandidates_.some(function(candidate_) {
var strCandidateName = (candidate_.name || '').trim().toLowerCase(),
tmpIsMatch = strCandidateName === strWantedName;
if(tmpIsMatch) {
rcMatch = candidate_;
}
return tmpIsMatch;
});
return rcMatch;
}
//--------------------------------------------------------------------------
//
// The extra payload a lookup will actually carry: whatever the caller
// supplied at request time, or, where it supplied nothing, whatever the
// field declared statically in its "exactMatchInfo".
//
// Answered in one place because two things need the same answer: the
// request itself, and the loop-breaker's key (see
// "pvBuildResolutionAttemptKey()").
//
//--------------------------------------------------------------------------
function pvResolveExactMatchAdditionalData(params_) {
var objExactMatchInfo = params_.exactMatchInfo,
objSuppliedAdditionalData = params_.additionalData;
return undefined === objSuppliedAdditionalData ?
(objExactMatchInfo || {}).additionalData :
objSuppliedAdditionalData;
}
//--------------------------------------------------------------------------
//
// The question a lookup is about to be put, as one string: the text to
// resolve together with whatever narrowing rides along with it.
//
// The narrowing belongs in here because it decides the answer. A field
// whose lookup honours a control the user can still change (see
// "PagedPicker.PickerRequestStage_Enum.prsExactMatchFetch") can be asked
// about one piece of text twice and be told different things, so a key made
// of the text alone would cache the first refusal and go on refusing a
// value that has since become legitimate.
//
// Serialized rather than compared field by field, since the narrowing is
// whatever shape its host chose. Two objects saying the same thing in a
// different property order read as different questions, which costs a round
// trip and never a wrong answer: that is the safe way round.
//
//--------------------------------------------------------------------------
function pvBuildResolutionAttemptKey(params_) {
var strSearchText = params_.searchText,
objAdditionalData = params_.additionalData;
return JSON.stringify({
searchText: strSearchText,
additionalData: objAdditionalData
});
}
//--------------------------------------------------------------------------
//
// The loop breaker. Records against the field itself the question a lookup
// has just answered, and says whether that same question has been answered
// before.
//
// Built in here rather than left to each caller (which is what every
// hand-rolled version of this has had to do) because without it a value
// that genuinely matches nothing would re-enter the caller, fail again,
// fire the same lookup again, and never terminate.
//
// Keyed on the whole question rather than being a plain "once" flag, so
// editing the value, or changing whatever narrows the lookup, asks the
// server again, while a question already answered is not asked twice. See
// "pvBuildResolutionAttemptKey()" for what a question is made of, and the
// caller for why the record waits on the answer.
//
//--------------------------------------------------------------------------
function pvResolutionAlreadyAttemptedFor(objTextField_, attemptKey_) {
var strLastKey = mjtElemData(objTextField_, 'lastResolutionAttemptKey');
return strLastKey === attemptKey_;
}
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
function pvRememberResolutionAttempt(objTextField_, attemptKey_) {
mjtElemData(objTextField_, 'lastResolutionAttemptKey', attemptKey_);
}
//--------------------------------------------------------------------------
//
// Hands a recovered match to the field's own explicit-selection hook, so
// that a host's reaction to a value arriving is the one it already has for
// a value picked from the dropdown and it needs no second commit path.
//
// Invoked off the field's own "typeAheadConfigParams" rather than the
// global "typeAheadDiv"'s, which by now describes whatever the user has
// touched since the lookup was fired.
//
// Only "fnHandleExplicitSelectionP" is consulted here, never the older
// "fnHandleExplicitSelection" that a field may declare in its place. The
// two differ in how the handler is called: the "P" one is handed a single
// "params_" object, whereas the older one is handed a fixed list of
// positional arguments, among them the selected row's own DOM element. The
// row synthesized below has no such element, which the "params_" contract
// allows for and the positional one says nothing about.
//
// A field declaring only "fnHandleExplicitSelection" still gets the
// recovered value: "pvCommitTypeAheadSelectionValue()" wrote its text and
// id fields before this ran. Only this call is skipped for it. That handler
// is still run by "pvHandleExplicitTypeAheadSelection()", which falls back
// to it when a row is picked out of the dropdown; it just never runs for a
// value recovered by lookup.
//
//--------------------------------------------------------------------------
function pvNotifyHostOfResolvedSelection(params_) {
var objTypeAheadConfigParams = params_.typeAheadConfigParams,
objMatch = params_.match,
fnHandleExplicitSelectionP =
StringificationUtils.getOrResolveFunction({
functionNameOrReference:
objTypeAheadConfigParams.fnHandleExplicitSelectionP,
descriptionOfFunctionBeingResolved:
'typeAheadConfigParams.fnHandleExplicitSelectionP',
suppressErrorsOnMissingNameOrReference: 1
});
if(fnHandleExplicitSelectionP) {
fnHandleExplicitSelectionP({
idxSelectedRow: 0,
selectedRow: null,
formId: objTypeAheadConfigParams.formId,
typeAheadConfigParams: objTypeAheadConfigParams,
explicitSelectionFnParameter:
objTypeAheadConfigParams.explicitSelectionFnParameter,
arrTypeaheadRowVals: [objMatch],
explicitSelectionResultOptions: {}
});
}
}
//--------------------------------------------------------------------------
//
// Commits the lookup's answer, when it holds one, into exactly the state a
// dropdown pick would have left behind: the display text and the hidden id
// field both written (see "pvCommitTypeAheadSelectionValue()"), and the
// host told. A response holding nothing that matches is left alone
// entirely, which is what lets the caller's next pass refuse the value with
// its own wording rather than a message owned by this framework.
//
//--------------------------------------------------------------------------
function pvCommitExactMatchIfFound(params_) {
var objTypeAheadConfigParams = params_.typeAheadConfigParams,
strSearchText = params_.searchText,
objMatch =
pvFindExactMatch(
params_.arrCandidates,
strSearchText);
if(objMatch) {
pvCommitTypeAheadSelectionValue({
typeAheadConfigParams: objTypeAheadConfigParams,
id: objMatch.id,
displayValue: objMatch.name
});
pvNotifyHostOfResolvedSelection({
typeAheadConfigParams: objTypeAheadConfigParams,
match: objMatch
});
}
}
//--------------------------------------------------------------------------
//
// Asks the field's dedicated exact-match command whether the typed text
// names a real option.
//
// A dedicated command rather than a rescan of what the dropdown last
// fetched: that feed is capped (see "MAX_TYPE_AHEAD_TO_RETRIEVE" and the
// "isMore" flag it sets), so a common prefix can truncate the exact match
// out of it and see a legitimate value refused.
//
// Buttons are deliberately left to the standard disable/enable handling
// here, unlike the dropdown's own fetch: this runs while a submit is held
// open on the answer, so a second submit landing in the middle is the
// thing to prevent.
//
// "additionalData" arrives already settled between what the caller asked
// for and what the field declared (see
// "pvResolveExactMatchAdditionalData()"), and is sent as it stands. That
// choice is not remade here, so that what rides on the request and what the
// loop breaker recorded cannot say different things.
//
//--------------------------------------------------------------------------
function pvRequestExactMatch(params_) {
var objTypeAheadConfigParams = params_.typeAheadConfigParams,
strSearchText = params_.searchText,
fnOnResolved = params_.fnOnResolved,
objAdditionalData = params_.additionalData,
objExactMatchInfo = objTypeAheadConfigParams.exactMatchInfo,
strArrayPropertyName = objExactMatchInfo.arrayPropertyName,
objIdHolder = {};
objIdHolder.expectedContextId =
jsrsExecuteWithErrorP(
objExactMatchInfo.commandString, // commandString_
rsCallbackHandleStandardJSONResponse, // fnCallback_
'Validating', // doingWhat_
{
term: strSearchText,
additionalExactMatchData: objAdditionalData
}, // payloadDocument_
{
idHolder: objIdHolder,
fnOnSuccess: function(json_) {
pvCommitExactMatchIfFound({
typeAheadConfigParams: objTypeAheadConfigParams,
searchText: strSearchText,
arrCandidates: json_[strArrayPropertyName] || []
});
fnOnResolved();
}
}); // options_
}
//--------------------------------------------------------------------------
//
// Asks the server whether text a user typed into an id-backed typeahead,
// and never picked out of the dropdown, is nonetheless the name of a real
// option; if it is, recovers that option's id so the field holds a
// complete value instead of being refused.
//
// Answers "ptrResolutionPending" when a lookup has been fired, in which
// case nothing is decided yet and the caller should commit nothing and say
// nothing: "fnOnResolved" re-enters it once there is an answer. Answers
// "ptrUnresolvedText" whenever no lookup will happen, which covers a field
// that declares no exact-match command, a caller with no way to be resumed,
// and a question a lookup has already answered (see
// "pvBuildResolutionAttemptKey()"). That verdict is a refusal the caller
// words itself; this deliberately shows no message of its own, so the
// wording stays wherever each host already keeps it.
//
// The field declares the lookup through "exactMatchInfo" on its
// "typeAheadConfigParams": a "commandString", the "arrayPropertyName" the
// matching options come back under, and optional "additionalData" passed
// along to the command. Declared there, rather than in any server-side
// contract, because hosts that hand-write their typeaheads in JavaScript
// would have no way to fill in anything only such a contract could
// populate.
//
//
// (Named) Parameters:
//
// additionalData: Optional extra payload for the lookup, replacing
// (never merged with) whatever
// "exactMatchInfo.additionalData" declared. For a caller
// whose narrowing is only known at submit time, such as
// the state of a control the user has since touched.
// Undefined means "nothing to say", leaving the declared
// value in place. Part of what the loop breaker keys on,
// so a caller answering differently than it did last
// time is asking a new question and gets a fresh lookup
// rather than the previous verdict.
//
// fnOnResolved: Called once the lookup answers, whether or not it
// matched anything, to re-enter whatever the caller was
// doing when it asked. Required: firing a lookup with no
// way to resume would abandon that work with the dialog
// still open, so a caller that supplies none is simply
// told the text is unresolved.
//
// formId: Id of the form the typeahead field was built onto.
//
// textFieldName: Name of the typeahead's visible text field, whose
// current contents are the text to resolve. The field's
// own configuration, and so the command string this
// needs, is found from the field itself (see
// "pvRememberTypeAheadConfigParamsForField()"), not
// passed in.
//
//--------------------------------------------------------------------------
function pbResolveUnmatchedTypeaheadText(params_) {
var strFormId = params_.formId,
strTextFieldName = params_.textFieldName,
fnOnResolved = params_.fnOnResolved,
objForm = getObj(strFormId),
objTextField = objForm ? objForm[strTextFieldName] : null,
objTypeAheadConfigParams =
pvGetTypeAheadConfigParamsForField(objTextField),
objExactMatchInfo =
(objTypeAheadConfigParams || {}).exactMatchInfo,
strSearchText = objTextField ? trim(objTextField.value) : '',
objAdditionalData =
pvResolveExactMatchAdditionalData({
exactMatchInfo: objExactMatchInfo,
additionalData: params_.additionalData
}),
strAttemptKey =
pvBuildResolutionAttemptKey({
searchText: strSearchText,
additionalData: objAdditionalData
}),
tmpCanAsk =
!!objTextField &&
!!fnOnResolved &&
!!strSearchText.length &&
!!(objExactMatchInfo || {}).commandString &&
!pvResolutionAlreadyAttemptedFor(objTextField, strAttemptKey),
rcResolution =
PendingTypeaheadResolution_Enum.ptrUnresolvedText;
if(tmpCanAsk) {
pvRequestExactMatch({
typeAheadConfigParams: objTypeAheadConfigParams,
searchText: strSearchText,
additionalData: objAdditionalData,
// Wrapped so the attempt is recorded by the answer rather than
// by the request, immediately before the caller is re-entered.
// Re-entry is the whole of what the loop breaker guards
// against, and a request that never answers never re-enters
// anybody, so recording one that did not arrive would refuse a
// perfectly good value on the strength of a lookup that failed.
// That matters because there is no one seam to clear it from: a
// lookup can go unanswered through a transport error, a
// response that fails "checkResponseForError()", or an
// "rsErrorObject" in the response, and the three are reported
// in three different ways.
fnOnResolved: function() {
pvRememberResolutionAttempt(objTextField, strAttemptKey);
fnOnResolved();
}
});
rcResolution =
PendingTypeaheadResolution_Enum.ptrResolutionPending;
}
return rcResolution;
}
//--------------------------------------------------------------------------
// TypeAheadPopup
//--------------------------------------------------------------------------
return {
isTypeaheadDropdownShowing: pbIsTypeaheadDropdownShowing,
prependAllItemToArray: pbPrependAllItemToArray,
buildStringifiedTypeaheadInfo: pbBuildStringifiedTypeaheadInfo,
resolveUnmatchedTypeaheadText: pbResolveUnmatchedTypeaheadText,
PendingTypeaheadResolution_Enum: PendingTypeaheadResolution_Enum,
hideTypeAheadDiv: pbHideTypeAheadDiv,
hideTypeAheadDivIfNotTypeAheadDiv: pbHideTypeAheadDivIfNotTypeAheadDiv,
rsCallbackGetTypeAheadSearchText: pbRSCallbackGetTypeAheadSearchText,
rsCallbackGetTypeAheadSearchTextJSON:
pbRSCallbackGetTypeAheadSearchTextJSON,
processTypeAheadSearchOfJSONArray:
pbProcessTypeAheadSearchOfJSONArray,
scrollTypeaheadSelectionIntoViewIfNecessary:
pbScrollTypeaheadSelectionIntoViewIfNecessary,
updateTypeAheadBoxTimerFired: pbUpdateTypeAheadBoxTimerFired,
handleTypeAheadTextBoxBlur: pbHandleTypeAheadTextBoxBlur,
handleTypeAheadTextBoxFocus: pbHandleTypeAheadTextBoxFocus,
handleTypeAheadTextBoxOnInput: pbHandleTypeAheadTextBoxOnInput,
handleTypeAheadTextBoxKeyDown: pbHandleTypeAheadTextBoxKeyDown,
simpleExplicitSelectionHandler: pbSimpleExplicitSelectionHandler,
setInvalidTextClass: pbSetInvalidTextClass,
PROTECTED: {
// Named by the inline event handlers the dropdown's own rows are
// built with, so these three are reached by name from the DOM (see
// "pvRenderTypeaheadDropdown()"), not by any other file.
selectTypeAheadItem: ptSelectTypeAheadItem,
setFocusToTextField: ptSetFocusToTextField,
removeTypeAheadSelectionOrHoverHighlight:
ptRemoveTypeAheadSelectionOrHoverHighlight
},
TEST: {
setSuppressHideDropdown: pvSetSuppressHideDropdown,
findExactMatch: pvFindExactMatch,
// Seeds a field's configuration the way a focus or keystroke would,
// so a spec can put a field into the state the resolver reads
// without dispatching real events at it.
rememberTypeAheadConfigParamsForField:
pvRememberTypeAheadConfigParamsForField,
// The auto-select rule, whose answer depends on the rows already
// offered and so cannot be read off any single call. Exposed so a
// spec can walk a whole row set past it.
applyExactMatchIfRowMatches: pvApplyExactMatchIfRowMatches,
// The render step that fills the dropdown and schedules its
// positioning. Exposed so a spec can reach the gap between the two
// without running a lookup.
finishTypeaheadRender: pvFinishTypeaheadRender
}
};
}());