123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- // This file contains javascript associated with a autosuggest control
- function smc_AutoSuggest(oOptions)
- {
- this.opt = oOptions;
- // Store the handle to the text box.
- this.oTextHandle = document.getElementById(this.opt.sControlId);
- this.oRealTextHandle = null;
- this.oSuggestDivHandle = null;
- this.sLastSearch = '';
- this.sLastDirtySearch = '';
- this.oSelectedDiv = null;
- this.aCache = [];
- this.aDisplayData = [];
- this.sRetrieveURL = 'sRetrieveURL' in this.opt ? this.opt.sRetrieveURL : '%scripturl%action=suggest;suggest_type=%suggest_type%;search=%search%;%sessionVar%=%sessionID%;xml;time=%time%';
- // How many objects can we show at once?
- this.iMaxDisplayQuantity = 'iMaxDisplayQuantity' in this.opt ? this.opt.iMaxDisplayQuantity : 15;
- // How many characters shall we start searching on?
- this.iMinimumSearchChars = 'iMinimumSearchChars' in this.opt ? this.opt.iMinimumSearchChars : 3;
- // Should selected items be added to a list?
- this.bItemList = 'bItemList' in this.opt ? this.opt.bItemList : false;
- // Are there any items that should be added in advance?
- this.aListItems = 'aListItems' in this.opt ? this.opt.aListItems : [];
- this.sItemTemplate = 'sItemTemplate' in this.opt ? this.opt.sItemTemplate : '<input type="hidden" name="%post_name%[]" value="%item_id%" /><a href="%item_href%" class="extern" onclick="window.open(this.href, \'_blank\'); return false;">%item_name%</a> <img src="%images_url%/pm_recipient_delete.png" alt="%delete_text%" title="%delete_text%" onclick="return %self%.deleteAddedItem(%item_id%);" />';
- this.sTextDeleteItem = 'sTextDeleteItem' in this.opt ? this.opt.sTextDeleteItem : '';
- this.oCallback = {};
- this.bDoAutoAdd = false;
- this.iItemCount = 0;
- this.oHideTimer = null;
- this.bPositionComplete = false;
- this.oXmlRequestHandle = null;
- // Just make sure the page is loaded before calling the init.
- addLoadEvent(this.opt.sSelf + '.init();');
- }
- smc_AutoSuggest.prototype.init = function()
- {
- if (!window.XMLHttpRequest)
- return false;
- // Create a div that'll contain the results later on.
- this.oSuggestDivHandle = document.createElement('div');
- this.oSuggestDivHandle.className = 'auto_suggest_div';
- document.body.appendChild(this.oSuggestDivHandle);
- // Create a backup text input.
- this.oRealTextHandle = document.createElement('input');
- this.oRealTextHandle.type = 'hidden';
- this.oRealTextHandle.name = this.oTextHandle.name;
- this.oRealTextHandle.value = this.oTextHandle.value;
- this.oTextHandle.form.appendChild(this.oRealTextHandle);
- // Disable autocomplete in any browser by obfuscating the name.
- this.oTextHandle.name = 'dummy_' + Math.floor(Math.random() * 1000000);
- this.oTextHandle.autocomplete = 'off';
- this.oTextHandle.instanceRef = this;
- var fOnKeyDown = function (oEvent) {
- return this.instanceRef.handleKey(oEvent);
- };
- is_opera ? this.oTextHandle.onkeypress = fOnKeyDown : this.oTextHandle.onkeydown = fOnKeyDown;
- this.oTextHandle.onkeyup = function (oEvent) {
- return this.instanceRef.autoSuggestUpdate(oEvent);
- };
- this.oTextHandle.onchange = function (oEvent) {
- return this.instanceRef.autoSuggestUpdate(oEvent);
- };
- this.oTextHandle.onblur = function (oEvent) {
- return this.instanceRef.autoSuggestHide(oEvent);
- };
- this.oTextHandle.onfocus = function (oEvent) {
- return this.instanceRef.autoSuggestUpdate(oEvent);
- };
- if (this.bItemList)
- {
- if ('sItemListContainerId' in this.opt)
- this.oItemList = document.getElementById(this.opt.sItemListContainerId);
- else
- {
- this.oItemList = document.createElement('div');
- this.oTextHandle.parentNode.insertBefore(this.oItemList, this.oTextHandle.nextSibling);
- }
- }
- if (this.aListItems.length > 0)
- for (var i = 0, n = this.aListItems.length; i < n; i++)
- this.addItemLink(this.aListItems[i].sItemId, this.aListItems[i].sItemName);
- return true;
- }
- // Was it an enter key - if so assume they are trying to select something.
- smc_AutoSuggest.prototype.handleKey = function(oEvent)
- {
- // Grab the event object, one way or the other
- if (!oEvent)
- oEvent = window.event;
- // Get the keycode of the key that was pressed.
- var iKeyPress = 0;
- if ('keyCode' in oEvent)
- iKeyPress = oEvent.keyCode;
- else if ('which' in oEvent)
- iKeyPress = oEvent.which;
- switch (iKeyPress)
- {
- // Tab.
- case 9:
- if (this.aDisplayData.length > 0)
- {
- if (this.oSelectedDiv != null)
- this.itemClicked(this.oSelectedDiv);
- else
- this.handleSubmit();
- }
- // Continue to the next control.
- return true;
- break;
- // Enter.
- case 13:
- if (this.aDisplayData.length > 0 && this.oSelectedDiv != null)
- {
- this.itemClicked(this.oSelectedDiv);
- // Do our best to stop it submitting the form!
- return false;
- }
- else
- return true;
- break;
- // Up/Down arrow?
- case 38:
- case 40:
- if (this.aDisplayData.length && this.oSuggestDivHandle.style.visibility != 'hidden')
- {
- // Loop through the display data trying to find our entry.
- var bPrevHandle = false;
- var oToHighlight = null;
- for (var i = 0; i < this.aDisplayData.length; i++)
- {
- // If we're going up and yet the top one was already selected don't go around.
- if (this.oSelectedDiv != null && this.oSelectedDiv == this.aDisplayData[i] && i == 0 && iKeyPress == 38)
- {
- oToHighlight = this.oSelectedDiv;
- break;
- }
- // If nothing is selected and we are going down then we select the first one.
- if (this.oSelectedDiv == null && iKeyPress == 40)
- {
- oToHighlight = this.aDisplayData[i];
- break;
- }
- // If the previous handle was the actual previously selected one and we're hitting down then this is the one we want.
- if (bPrevHandle != false && bPrevHandle == this.oSelectedDiv && iKeyPress == 40)
- {
- oToHighlight = this.aDisplayData[i];
- break;
- }
- // If we're going up and this is the previously selected one then we want the one before, if there was one.
- if (bPrevHandle != false && this.aDisplayData[i] == this.oSelectedDiv && iKeyPress == 38)
- {
- oToHighlight = bPrevHandle;
- break;
- }
- // Make the previous handle this!
- bPrevHandle = this.aDisplayData[i];
- }
- // If we don't have one to highlight by now then it must be the last one that we're after.
- if (oToHighlight == null)
- oToHighlight = bPrevHandle;
- // Remove any old highlighting.
- if (this.oSelectedDiv != null)
- this.itemMouseOut(this.oSelectedDiv);
- // Mark what the selected div now is.
- this.oSelectedDiv = oToHighlight;
- this.itemMouseOver(this.oSelectedDiv);
- }
- break;
- }
- return true;
- }
- // Functions for integration.
- smc_AutoSuggest.prototype.registerCallback = function(sCallbackType, sCallback)
- {
- switch (sCallbackType)
- {
- case 'onBeforeAddItem':
- this.oCallback.onBeforeAddItem = sCallback;
- break;
- case 'onAfterAddItem':
- this.oCallback.onAfterAddItem = sCallback;
- break;
- case 'onAfterDeleteItem':
- this.oCallback.onAfterDeleteItem = sCallback;
- break;
- case 'onBeforeUpdate':
- this.oCallback.onBeforeUpdate = sCallback;
- break;
- }
- }
- // User hit submit?
- smc_AutoSuggest.prototype.handleSubmit = function()
- {
- var bReturnValue = true;
- var oFoundEntry = null;
- // Do we have something that matches the current text?
- for (var i = 0; i < this.aCache.length; i++)
- {
- if (this.sLastSearch.toLowerCase() == this.aCache[i].sItemName.toLowerCase().substr(0, this.sLastSearch.length))
- {
- // Exact match?
- if (this.sLastSearch.length == this.aCache[i].sItemName.length)
- {
- // This is the one!
- oFoundEntry = {
- sItemId: this.aCache[i].sItemId,
- sItemName: this.aCache[i].sItemName
- };
- break;
- }
- // Not an exact match, but it'll do for now.
- else
- {
- // If we have two matches don't find anything.
- if (oFoundEntry != null)
- bReturnValue = false;
- else
- oFoundEntry = {
- sItemId: this.aCache[i].sItemId,
- sItemName: this.aCache[i].sItemName
- };
- }
- }
- }
- if (oFoundEntry == null || bReturnValue == false)
- return bReturnValue;
- else
- {
- this.addItemLink(oFoundEntry.sItemId, oFoundEntry.sItemName, true);
- return false;
- }
- }
- // Positions the box correctly on the window.
- smc_AutoSuggest.prototype.positionDiv = function()
- {
- // Only do it once.
- if (this.bPositionComplete)
- return true;
- this.bPositionComplete = true;
- // Put the div under the text box.
- var aParentPos = smf_itemPos(this.oTextHandle);
- this.oSuggestDivHandle.style.left = aParentPos[0] + 'px';
- this.oSuggestDivHandle.style.top = (aParentPos[1] + this.oTextHandle.offsetHeight) + 'px';
- this.oSuggestDivHandle.style.width = this.oTextHandle.style.width;
- return true;
- }
- // Do something after clicking an item.
- smc_AutoSuggest.prototype.itemClicked = function(oCurElement)
- {
- // Is there a div that we are populating?
- if (this.bItemList)
- this.addItemLink(oCurElement.sItemId, oCurElement.innerHTML);
- // Otherwise clear things down.
- else
- this.oTextHandle.value = oCurElement.innerHTML.php_unhtmlspecialchars();
- this.oRealTextHandle.value = this.oTextHandle.value;
- this.autoSuggestActualHide();
- this.oSelectedDiv = null;
- }
- // Remove the last searched for name from the search box.
- smc_AutoSuggest.prototype.removeLastSearchString = function ()
- {
- // Remove the text we searched for from the div.
- var sTempText = this.oTextHandle.value.toLowerCase();
- var iStartString = sTempText.indexOf(this.sLastSearch.toLowerCase());
- // Just attempt to remove the bits we just searched for.
- if (iStartString != -1)
- {
- while (iStartString > 0)
- {
- if (sTempText.charAt(iStartString - 1) == '"' || sTempText.charAt(iStartString - 1) == ',' || sTempText.charAt(iStartString - 1) == ' ')
- {
- iStartString--;
- if (sTempText.charAt(iStartString - 1) == ',')
- break;
- }
- else
- break;
- }
- // Now remove anything from iStartString upwards.
- this.oTextHandle.value = this.oTextHandle.value.substr(0, iStartString);
- }
- // Just take it all.
- else
- this.oTextHandle.value = '';
- }
- // Add a result if not already done.
- smc_AutoSuggest.prototype.addItemLink = function (sItemId, sItemName, bFromSubmit)
- {
- // Increase the internal item count.
- this.iItemCount ++;
- // If there's a callback then call it.
- if ('oCallback' in this && 'onBeforeAddItem' in this.oCallback && typeof(this.oCallback.onBeforeAddItem) == 'string')
- {
- // If it returns false the item must not be added.
- if (!eval(this.oCallback.onBeforeAddItem + '(' + this.opt.sSelf + ', \'' + sItemId + '\');'))
- return;
- }
- var oNewDiv = document.createElement('div');
- oNewDiv.id = 'suggest_' + this.opt.sSuggestId + '_' + sItemId;
- setInnerHTML(oNewDiv, this.sItemTemplate.replace(/%post_name%/g, this.opt.sPostName).replace(/%item_id%/g, sItemId).replace(/%item_href%/g, smf_prepareScriptUrl(smf_scripturl) + this.opt.sURLMask.replace(/%item_id%/g, sItemId)).replace(/%item_name%/g, sItemName).replace(/%images_url%/g, smf_images_url).replace(/%self%/g, this.opt.sSelf).replace(/%delete_text%/g, this.sTextDeleteItem));
- this.oItemList.appendChild(oNewDiv);
- // If there's a registered callback, call it.
- if ('oCallback' in this && 'onAfterAddItem' in this.oCallback && typeof(this.oCallback.onAfterAddItem) == 'string')
- eval(this.oCallback.onAfterAddItem + '(' + this.opt.sSelf + ', \'' + oNewDiv.id + '\', ' + this.iItemCount + ');');
- // Clear the div a bit.
- this.removeLastSearchString();
- // If we came from a submit, and there's still more to go, turn on auto add for all the other things.
- this.bDoAutoAdd = this.oTextHandle.value != '' && bFromSubmit;
- // Update the fellow..
- this.autoSuggestUpdate();
- }
- // Delete an item that has been added, if at all?
- smc_AutoSuggest.prototype.deleteAddedItem = function (sItemId)
- {
- var oDiv = document.getElementById('suggest_' + this.opt.sSuggestId + '_' + sItemId);
- // Remove the div if it exists.
- if (typeof(oDiv) == 'object' && oDiv != null)
- {
- oDiv.parentNode.removeChild(document.getElementById('suggest_' + this.opt.sSuggestId + '_' + sItemId));
- // Decrease the internal item count.
- this.iItemCount --;
- // If there's a registered callback, call it.
- if ('oCallback' in this && 'onAfterDeleteItem' in this.oCallback && typeof(this.oCallback.onAfterDeleteItem) == 'string')
- eval(this.oCallback.onAfterDeleteItem + '(' + this.opt.sSelf + ', ' + this.iItemCount + ');');
- }
- return false;
- }
- // Hide the box.
- smc_AutoSuggest.prototype.autoSuggestHide = function ()
- {
- // Delay to allow events to propogate through....
- this.oHideTimer = setTimeout(this.opt.sSelf + '.autoSuggestActualHide();', 250);
- }
- // Do the actual hiding after a timeout.
- smc_AutoSuggest.prototype.autoSuggestActualHide = function()
- {
- this.oSuggestDivHandle.style.display = 'none';
- this.oSuggestDivHandle.style.visibility = 'hidden';
- this.oSelectedDiv = null;
- }
- // Show the box.
- smc_AutoSuggest.prototype.autoSuggestShow = function()
- {
- if (this.oHideTimer)
- {
- clearTimeout(this.oHideTimer);
- this.oHideTimer = false;
- }
- this.positionDiv();
- this.oSuggestDivHandle.style.visibility = 'visible';
- this.oSuggestDivHandle.style.display = '';
- }
- // Populate the actual div.
- smc_AutoSuggest.prototype.populateDiv = function(aResults)
- {
- // Cannot have any children yet.
- while (this.oSuggestDivHandle.childNodes.length > 0)
- {
- // Tidy up the events etc too.
- this.oSuggestDivHandle.childNodes[0].onmouseover = null;
- this.oSuggestDivHandle.childNodes[0].onmouseout = null;
- this.oSuggestDivHandle.childNodes[0].onclick = null;
- this.oSuggestDivHandle.removeChild(this.oSuggestDivHandle.childNodes[0]);
- }
- // Something to display?
- if (typeof(aResults) == 'undefined')
- {
- this.aDisplayData = [];
- return false;
- }
- var aNewDisplayData = [];
- for (var i = 0; i < (aResults.length > this.iMaxDisplayQuantity ? this.iMaxDisplayQuantity : aResults.length); i++)
- {
- // Create the sub element
- var oNewDivHandle = document.createElement('div');
- oNewDivHandle.sItemId = aResults[i].sItemId;
- oNewDivHandle.className = 'auto_suggest_item';
- oNewDivHandle.innerHTML = aResults[i].sItemName;
- //oNewDivHandle.style.width = this.oTextHandle.style.width;
- this.oSuggestDivHandle.appendChild(oNewDivHandle);
- // Attach some events to it so we can do stuff.
- oNewDivHandle.instanceRef = this;
- oNewDivHandle.onmouseover = function (oEvent)
- {
- this.instanceRef.itemMouseOver(this);
- }
- oNewDivHandle.onmouseout = function (oEvent)
- {
- this.instanceRef.itemMouseOut(this);
- }
- oNewDivHandle.onclick = function (oEvent)
- {
- this.instanceRef.itemClicked(this);
- }
- aNewDisplayData[i] = oNewDivHandle;
- }
- this.aDisplayData = aNewDisplayData;
- return true;
- }
- // Refocus the element.
- smc_AutoSuggest.prototype.itemMouseOver = function (oCurElement)
- {
- this.oSelectedDiv = oCurElement;
- oCurElement.className = 'auto_suggest_item_hover';
- }
- // Onfocus the element
- smc_AutoSuggest.prototype.itemMouseOut = function (oCurElement)
- {
- oCurElement.className = 'auto_suggest_item';
- }
- smc_AutoSuggest.prototype.onSuggestionReceived = function (oXMLDoc)
- {
- var sQuoteText = '';
- var aItems = oXMLDoc.getElementsByTagName('item');
- this.aCache = [];
- for (var i = 0; i < aItems.length; i++)
- {
- this.aCache[i] = {
- sItemId: aItems[i].getAttribute('id'),
- sItemName: aItems[i].childNodes[0].nodeValue
- };
- // If we're doing auto add and we find the exact person, then add them!
- if (this.bDoAutoAdd && this.sLastSearch == this.aCache[i].sItemName)
- {
- var oReturnValue = {
- sItemId: this.aCache[i].sItemId,
- sItemName: this.aCache[i].sItemName
- };
- this.aCache = [];
- return this.addItemLink(oReturnValue.sItemId, oReturnValue.sItemName, true);
- }
- }
- // Check we don't try to keep auto updating!
- this.bDoAutoAdd = false;
- // Populate the div.
- this.populateDiv(this.aCache);
- // Make sure we can see it - if we can.
- if (aItems.length == 0)
- this.autoSuggestHide();
- else
- this.autoSuggestShow();
- return true;
- }
- // Get a new suggestion.
- smc_AutoSuggest.prototype.autoSuggestUpdate = function ()
- {
- // If there's a callback then call it.
- if ('onBeforeUpdate' in this.oCallback && typeof(this.oCallback.onBeforeUpdate) == 'string')
- {
- // If it returns false the item must not be added.
- if (!eval(this.oCallback.onBeforeUpdate + '(' + this.opt.sSelf + ');'))
- return false;
- }
- this.oRealTextHandle.value = this.oTextHandle.value;
- if (isEmptyText(this.oTextHandle))
- {
- this.aCache = [];
- this.populateDiv();
- this.autoSuggestHide();
- return true;
- }
- // Nothing changed?
- if (this.oTextHandle.value == this.sLastDirtySearch)
- return true;
- this.sLastDirtySearch = this.oTextHandle.value;
- // We're only actually interested in the last string.
- var sSearchString = this.oTextHandle.value.replace(/^("[^"]+",[ ]*)+/, '').replace(/^([^,]+,[ ]*)+/, '');
- if (sSearchString.substr(0, 1) == '"')
- sSearchString = sSearchString.substr(1);
- // Stop replication ASAP.
- var sRealLastSearch = this.sLastSearch;
- this.sLastSearch = sSearchString;
- // Either nothing or we've completed a sentance.
- if (sSearchString == '' || sSearchString.substr(sSearchString.length - 1) == '"')
- {
- this.populateDiv();
- return true;
- }
- // Nothing?
- if (sRealLastSearch == sSearchString)
- return true;
- // Too small?
- else if (sSearchString.length < this.iMinimumSearchChars)
- {
- this.aCache = [];
- this.autoSuggestHide();
- return true;
- }
- else if (sSearchString.substr(0, sRealLastSearch.length) == sRealLastSearch)
- {
- // Instead of hitting the server again, just narrow down the results...
- var aNewCache = [];
- var j = 0;
- var sLowercaseSearch = sSearchString.toLowerCase();
- for (var k = 0; k < this.aCache.length; k++)
- {
- if (this.aCache[k].sItemName.substr(0, sSearchString.length).toLowerCase() == sLowercaseSearch)
- aNewCache[j++] = this.aCache[k];
- }
- this.aCache = [];
- if (aNewCache.length != 0)
- {
- this.aCache = aNewCache;
- // Repopulate.
- this.populateDiv(this.aCache);
- // Check it can be seen.
- this.autoSuggestShow();
- return true;
- }
- }
- // In progress means destroy!
- if (typeof(this.oXmlRequestHandle) == 'object' && this.oXmlRequestHandle != null)
- this.oXmlRequestHandle.abort();
- // Clean the text handle.
- sSearchString = sSearchString.php_to8bit().php_urlencode();
- // Get the document.
- this.tmpMethod = getXMLDocument;
- this.oXmlRequestHandle = this.tmpMethod(this.sRetrieveURL.replace(/%scripturl%/g, smf_prepareScriptUrl(smf_scripturl)).replace(/%suggest_type%/g, this.opt.sSearchType).replace(/%search%/g, sSearchString).replace(/%sessionVar%/g, this.opt.sSessionVar).replace(/%sessionID%/g, this.opt.sSessionId).replace(/%time%/g, new Date().getTime()), this.onSuggestionReceived);
- delete this.tmpMethod;
- return true;
- }
|