comparison forum/Themes/Vamp/scripts/suggest.js @ 76:e3e11437ecea website

Add forum code
author Chris Cannam
date Sun, 07 Jul 2013 11:25:48 +0200
parents
children
comparison
equal deleted inserted replaced
75:72f59aa7e503 76:e3e11437ecea
1 // This file contains javascript associated with a autosuggest control
2 function smc_AutoSuggest(oOptions)
3 {
4 this.opt = oOptions;
5
6 // Store the handle to the text box.
7 this.oTextHandle = document.getElementById(this.opt.sControlId);
8 this.oRealTextHandle = null;
9
10 this.oSuggestDivHandle = null;
11 this.sLastSearch = '';
12 this.sLastDirtySearch = '';
13 this.oSelectedDiv = null;
14 this.aCache = [];
15 this.aDisplayData = [];
16
17 this.sRetrieveURL = 'sRetrieveURL' in this.opt ? this.opt.sRetrieveURL : '%scripturl%action=suggest;suggest_type=%suggest_type%;search=%search%;%sessionVar%=%sessionID%;xml;time=%time%';
18
19 // How many objects can we show at once?
20 this.iMaxDisplayQuantity = 'iMaxDisplayQuantity' in this.opt ? this.opt.iMaxDisplayQuantity : 15;
21
22 // How many characters shall we start searching on?
23 this.iMinimumSearchChars = 'iMinimumSearchChars' in this.opt ? this.opt.iMinimumSearchChars : 3;
24
25 // Should selected items be added to a list?
26 this.bItemList = 'bItemList' in this.opt ? this.opt.bItemList : false;
27
28 // Are there any items that should be added in advance?
29 this.aListItems = 'aListItems' in this.opt ? this.opt.aListItems : [];
30
31 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>&nbsp;<img src="%images_url%/pm_recipient_delete.gif" alt="%delete_text%" title="%delete_text%" onclick="return %self%.deleteAddedItem(%item_id%);" />';
32
33 this.sTextDeleteItem = 'sTextDeleteItem' in this.opt ? this.opt.sTextDeleteItem : '';
34
35 this.oCallback = {};
36 this.bDoAutoAdd = false;
37 this.iItemCount = 0;
38
39 this.oHideTimer = null;
40 this.bPositionComplete = false;
41
42 this.oXmlRequestHandle = null;
43
44 // Just make sure the page is loaded before calling the init.
45 addLoadEvent(this.opt.sSelf + '.init();');
46 }
47
48 smc_AutoSuggest.prototype.init = function()
49 {
50 if (!window.XMLHttpRequest)
51 return false;
52
53 // Create a div that'll contain the results later on.
54 this.oSuggestDivHandle = document.createElement('div');
55 this.oSuggestDivHandle.className = 'auto_suggest_div';
56 document.body.appendChild(this.oSuggestDivHandle);
57
58 // Create a backup text input.
59 this.oRealTextHandle = document.createElement('input');
60 this.oRealTextHandle.type = 'hidden';
61 this.oRealTextHandle.name = this.oTextHandle.name;
62 this.oRealTextHandle.value = this.oTextHandle.value;
63 this.oTextHandle.form.appendChild(this.oRealTextHandle);
64
65 // Disable autocomplete in any browser by obfuscating the name.
66 this.oTextHandle.name = 'dummy_' + Math.floor(Math.random() * 1000000);
67 this.oTextHandle.autocomplete = 'off';
68
69 this.oTextHandle.instanceRef = this;
70
71 var fOnKeyDown = function (oEvent) {
72 return this.instanceRef.handleKey(oEvent);
73 };
74 is_opera ? this.oTextHandle.onkeypress = fOnKeyDown : this.oTextHandle.onkeydown = fOnKeyDown;
75
76 this.oTextHandle.onkeyup = function (oEvent) {
77 return this.instanceRef.autoSuggestUpdate(oEvent);
78 };
79
80 this.oTextHandle.onchange = function (oEvent) {
81 return this.instanceRef.autoSuggestUpdate(oEvent);
82 };
83
84 this.oTextHandle.onblur = function (oEvent) {
85 return this.instanceRef.autoSuggestHide(oEvent);
86 };
87
88 this.oTextHandle.onfocus = function (oEvent) {
89 return this.instanceRef.autoSuggestUpdate(oEvent);
90 };
91
92 if (this.bItemList)
93 {
94 if ('sItemListContainerId' in this.opt)
95 this.oItemList = document.getElementById(this.opt.sItemListContainerId);
96 else
97 {
98 this.oItemList = document.createElement('div');
99 this.oTextHandle.parentNode.insertBefore(this.oItemList, this.oTextHandle.nextSibling);
100 }
101 }
102
103 if (this.aListItems.length > 0)
104 for (var i = 0, n = this.aListItems.length; i < n; i++)
105 this.addItemLink(this.aListItems[i].sItemId, this.aListItems[i].sItemName);
106
107 return true;
108 }
109
110 // Was it an enter key - if so assume they are trying to select something.
111 smc_AutoSuggest.prototype.handleKey = function(oEvent)
112 {
113 // Grab the event object, one way or the other
114 if (!oEvent)
115 oEvent = window.event;
116
117 // Get the keycode of the key that was pressed.
118 var iKeyPress = 0;
119 if ('keyCode' in oEvent)
120 iKeyPress = oEvent.keyCode;
121 else if ('which' in oEvent)
122 iKeyPress = oEvent.which;
123
124 switch (iKeyPress)
125 {
126 // Tab.
127 case 9:
128 if (this.aDisplayData.length > 0)
129 {
130 if (this.oSelectedDiv != null)
131 this.itemClicked(this.oSelectedDiv);
132 else
133 this.handleSubmit();
134 }
135
136 // Continue to the next control.
137 return true;
138 break;
139
140 // Enter.
141 case 13:
142 if (this.aDisplayData.length > 0 && this.oSelectedDiv != null)
143 {
144 this.itemClicked(this.oSelectedDiv);
145
146 // Do our best to stop it submitting the form!
147 return false;
148 }
149 else
150 return true;
151
152 break;
153
154 // Up/Down arrow?
155 case 38:
156 case 40:
157 if (this.aDisplayData.length && this.oSuggestDivHandle.style.visibility != 'hidden')
158 {
159 // Loop through the display data trying to find our entry.
160 var bPrevHandle = false;
161 var oToHighlight = null;
162 for (var i = 0; i < this.aDisplayData.length; i++)
163 {
164 // If we're going up and yet the top one was already selected don't go around.
165 if (this.oSelectedDiv != null && this.oSelectedDiv == this.aDisplayData[i] && i == 0 && iKeyPress == 38)
166 {
167 oToHighlight = this.oSelectedDiv;
168 break;
169 }
170 // If nothing is selected and we are going down then we select the first one.
171 if (this.oSelectedDiv == null && iKeyPress == 40)
172 {
173 oToHighlight = this.aDisplayData[i];
174 break;
175 }
176
177 // If the previous handle was the actual previously selected one and we're hitting down then this is the one we want.
178 if (bPrevHandle != false && bPrevHandle == this.oSelectedDiv && iKeyPress == 40)
179 {
180 oToHighlight = this.aDisplayData[i];
181 break;
182 }
183 // If we're going up and this is the previously selected one then we want the one before, if there was one.
184 if (bPrevHandle != false && this.aDisplayData[i] == this.oSelectedDiv && iKeyPress == 38)
185 {
186 oToHighlight = bPrevHandle;
187 break;
188 }
189 // Make the previous handle this!
190 bPrevHandle = this.aDisplayData[i];
191 }
192
193 // If we don't have one to highlight by now then it must be the last one that we're after.
194 if (oToHighlight == null)
195 oToHighlight = bPrevHandle;
196
197 // Remove any old highlighting.
198 if (this.oSelectedDiv != null)
199 this.itemMouseOut(this.oSelectedDiv);
200 // Mark what the selected div now is.
201 this.oSelectedDiv = oToHighlight;
202 this.itemMouseOver(this.oSelectedDiv);
203 }
204 break;
205 }
206 return true;
207 }
208
209 // Functions for integration.
210 smc_AutoSuggest.prototype.registerCallback = function(sCallbackType, sCallback)
211 {
212 switch (sCallbackType)
213 {
214 case 'onBeforeAddItem':
215 this.oCallback.onBeforeAddItem = sCallback;
216 break;
217
218 case 'onAfterAddItem':
219 this.oCallback.onAfterAddItem = sCallback;
220 break;
221
222 case 'onAfterDeleteItem':
223 this.oCallback.onAfterDeleteItem = sCallback;
224 break;
225
226 case 'onBeforeUpdate':
227 this.oCallback.onBeforeUpdate = sCallback;
228 break;
229 }
230 }
231
232 // User hit submit?
233 smc_AutoSuggest.prototype.handleSubmit = function()
234 {
235 var bReturnValue = true;
236 var oFoundEntry = null;
237
238 // Do we have something that matches the current text?
239 for (var i = 0; i < this.aCache.length; i++)
240 {
241 if (this.sLastSearch.toLowerCase() == this.aCache[i].sItemName.toLowerCase().substr(0, this.sLastSearch.length))
242 {
243 // Exact match?
244 if (this.sLastSearch.length == this.aCache[i].sItemName.length)
245 {
246 // This is the one!
247 oFoundEntry = {
248 sItemId: this.aCache[i].sItemId,
249 sItemName: this.aCache[i].sItemName
250 };
251 break;
252 }
253
254 // Not an exact match, but it'll do for now.
255 else
256 {
257 // If we have two matches don't find anything.
258 if (oFoundEntry != null)
259 bReturnValue = false;
260 else
261 oFoundEntry = {
262 sItemId: this.aCache[i].sItemId,
263 sItemName: this.aCache[i].sItemName
264 };
265 }
266 }
267 }
268
269 if (oFoundEntry == null || bReturnValue == false)
270 return bReturnValue;
271 else
272 {
273 this.addItemLink(oFoundEntry.sItemId, oFoundEntry.sItemName, true);
274 return false;
275 }
276 }
277
278 // Positions the box correctly on the window.
279 smc_AutoSuggest.prototype.positionDiv = function()
280 {
281 // Only do it once.
282 if (this.bPositionComplete)
283 return true;
284
285 this.bPositionComplete = true;
286
287 // Put the div under the text box.
288 var aParentPos = smf_itemPos(this.oTextHandle);
289
290 this.oSuggestDivHandle.style.left = aParentPos[0] + 'px';
291 this.oSuggestDivHandle.style.top = (aParentPos[1] + this.oTextHandle.offsetHeight) + 'px';
292 this.oSuggestDivHandle.style.width = this.oTextHandle.style.width;
293
294 return true;
295 }
296
297 // Do something after clicking an item.
298 smc_AutoSuggest.prototype.itemClicked = function(oCurElement)
299 {
300 // Is there a div that we are populating?
301 if (this.bItemList)
302 this.addItemLink(oCurElement.sItemId, oCurElement.innerHTML);
303
304 // Otherwise clear things down.
305 else
306 this.oTextHandle.value = oCurElement.innerHTML.php_unhtmlspecialchars();
307
308 this.oRealTextHandle.value = this.oTextHandle.value;
309 this.autoSuggestActualHide();
310 this.oSelectedDiv = null;
311 }
312
313 // Remove the last searched for name from the search box.
314 smc_AutoSuggest.prototype.removeLastSearchString = function ()
315 {
316 // Remove the text we searched for from the div.
317 var sTempText = this.oTextHandle.value.toLowerCase();
318 var iStartString = sTempText.indexOf(this.sLastSearch.toLowerCase());
319 // Just attempt to remove the bits we just searched for.
320 if (iStartString != -1)
321 {
322 while (iStartString > 0)
323 {
324 if (sTempText.charAt(iStartString - 1) == '"' || sTempText.charAt(iStartString - 1) == ',' || sTempText.charAt(iStartString - 1) == ' ')
325 {
326 iStartString--;
327 if (sTempText.charAt(iStartString - 1) == ',')
328 break;
329 }
330 else
331 break;
332 }
333
334 // Now remove anything from iStartString upwards.
335 this.oTextHandle.value = this.oTextHandle.value.substr(0, iStartString);
336 }
337 // Just take it all.
338 else
339 this.oTextHandle.value = '';
340 }
341
342 // Add a result if not already done.
343 smc_AutoSuggest.prototype.addItemLink = function (sItemId, sItemName, bFromSubmit)
344 {
345 // Increase the internal item count.
346 this.iItemCount ++;
347
348 // If there's a callback then call it.
349 if ('oCallback' in this && 'onBeforeAddItem' in this.oCallback && typeof(this.oCallback.onBeforeAddItem) == 'string')
350 {
351 // If it returns false the item must not be added.
352 if (!eval(this.oCallback.onBeforeAddItem + '(' + this.opt.sSelf + ', \'' + sItemId + '\');'))
353 return;
354 }
355
356 var oNewDiv = document.createElement('div');
357 oNewDiv.id = 'suggest_' + this.opt.sSuggestId + '_' + sItemId;
358 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));
359 this.oItemList.appendChild(oNewDiv);
360
361 // If there's a registered callback, call it.
362 if ('oCallback' in this && 'onAfterAddItem' in this.oCallback && typeof(this.oCallback.onAfterAddItem) == 'string')
363 eval(this.oCallback.onAfterAddItem + '(' + this.opt.sSelf + ', \'' + oNewDiv.id + '\', ' + this.iItemCount + ');');
364
365 // Clear the div a bit.
366 this.removeLastSearchString();
367
368 // If we came from a submit, and there's still more to go, turn on auto add for all the other things.
369 this.bDoAutoAdd = this.oTextHandle.value != '' && bFromSubmit;
370
371 // Update the fellow..
372 this.autoSuggestUpdate();
373 }
374
375 // Delete an item that has been added, if at all?
376 smc_AutoSuggest.prototype.deleteAddedItem = function (sItemId)
377 {
378 var oDiv = document.getElementById('suggest_' + this.opt.sSuggestId + '_' + sItemId);
379
380 // Remove the div if it exists.
381 if (typeof(oDiv) == 'object' && oDiv != null)
382 {
383 oDiv.parentNode.removeChild(document.getElementById('suggest_' + this.opt.sSuggestId + '_' + sItemId));
384
385 // Decrease the internal item count.
386 this.iItemCount --;
387
388 // If there's a registered callback, call it.
389 if ('oCallback' in this && 'onAfterDeleteItem' in this.oCallback && typeof(this.oCallback.onAfterDeleteItem) == 'string')
390 eval(this.oCallback.onAfterDeleteItem + '(' + this.opt.sSelf + ', ' + this.iItemCount + ');');
391 }
392
393 return false;
394 }
395
396 // Hide the box.
397 smc_AutoSuggest.prototype.autoSuggestHide = function ()
398 {
399 // Delay to allow events to propogate through....
400 this.oHideTimer = setTimeout(this.opt.sSelf + '.autoSuggestActualHide();', 250);
401 }
402
403 // Do the actual hiding after a timeout.
404 smc_AutoSuggest.prototype.autoSuggestActualHide = function()
405 {
406 this.oSuggestDivHandle.style.display = 'none';
407 this.oSuggestDivHandle.style.visibility = 'hidden';
408 this.oSelectedDiv = null;
409 }
410
411 // Show the box.
412 smc_AutoSuggest.prototype.autoSuggestShow = function()
413 {
414 if (this.oHideTimer)
415 {
416 clearTimeout(this.oHideTimer);
417 this.oHideTimer = false;
418 }
419
420 this.positionDiv();
421
422 this.oSuggestDivHandle.style.visibility = 'visible';
423 this.oSuggestDivHandle.style.display = '';
424 }
425
426 // Populate the actual div.
427 smc_AutoSuggest.prototype.populateDiv = function(aResults)
428 {
429 // Cannot have any children yet.
430 while (this.oSuggestDivHandle.childNodes.length > 0)
431 {
432 // Tidy up the events etc too.
433 this.oSuggestDivHandle.childNodes[0].onmouseover = null;
434 this.oSuggestDivHandle.childNodes[0].onmouseout = null;
435 this.oSuggestDivHandle.childNodes[0].onclick = null;
436
437 this.oSuggestDivHandle.removeChild(this.oSuggestDivHandle.childNodes[0]);
438 }
439
440 // Something to display?
441 if (typeof(aResults) == 'undefined')
442 {
443 this.aDisplayData = [];
444 return false;
445 }
446
447 var aNewDisplayData = [];
448 for (var i = 0; i < (aResults.length > this.iMaxDisplayQuantity ? this.iMaxDisplayQuantity : aResults.length); i++)
449 {
450 // Create the sub element
451 var oNewDivHandle = document.createElement('div');
452 oNewDivHandle.sItemId = aResults[i].sItemId;
453 oNewDivHandle.className = 'auto_suggest_item';
454 oNewDivHandle.innerHTML = aResults[i].sItemName;
455 //oNewDivHandle.style.width = this.oTextHandle.style.width;
456
457 this.oSuggestDivHandle.appendChild(oNewDivHandle);
458
459 // Attach some events to it so we can do stuff.
460 oNewDivHandle.instanceRef = this;
461 oNewDivHandle.onmouseover = function (oEvent)
462 {
463 this.instanceRef.itemMouseOver(this);
464 }
465 oNewDivHandle.onmouseout = function (oEvent)
466 {
467 this.instanceRef.itemMouseOut(this);
468 }
469 oNewDivHandle.onclick = function (oEvent)
470 {
471 this.instanceRef.itemClicked(this);
472 }
473
474
475 aNewDisplayData[i] = oNewDivHandle;
476 }
477
478 this.aDisplayData = aNewDisplayData;
479
480 return true;
481 }
482
483 // Refocus the element.
484 smc_AutoSuggest.prototype.itemMouseOver = function (oCurElement)
485 {
486 this.oSelectedDiv = oCurElement;
487 oCurElement.className = 'auto_suggest_item_hover';
488 }
489
490 // Onfocus the element
491 smc_AutoSuggest.prototype.itemMouseOut = function (oCurElement)
492 {
493 oCurElement.className = 'auto_suggest_item';
494 }
495
496 smc_AutoSuggest.prototype.onSuggestionReceived = function (oXMLDoc)
497 {
498 var sQuoteText = '';
499 var aItems = oXMLDoc.getElementsByTagName('item');
500 this.aCache = [];
501 for (var i = 0; i < aItems.length; i++)
502 {
503 this.aCache[i] = {
504 sItemId: aItems[i].getAttribute('id'),
505 sItemName: aItems[i].childNodes[0].nodeValue
506 };
507
508 // If we're doing auto add and we find the exact person, then add them!
509 if (this.bDoAutoAdd && this.sLastSearch == this.aCache[i].sItemName)
510 {
511 var oReturnValue = {
512 sItemId: this.aCache[i].sItemId,
513 sItemName: this.aCache[i].sItemName
514 };
515 this.aCache = [];
516 return this.addItemLink(oReturnValue.sItemId, oReturnValue.sItemName, true);
517 }
518 }
519
520 // Check we don't try to keep auto updating!
521 this.bDoAutoAdd = false;
522
523 // Populate the div.
524 this.populateDiv(this.aCache);
525
526 // Make sure we can see it - if we can.
527 if (aItems.length == 0)
528 this.autoSuggestHide();
529 else
530 this.autoSuggestShow();
531
532 return true;
533 }
534
535 // Get a new suggestion.
536 smc_AutoSuggest.prototype.autoSuggestUpdate = function ()
537 {
538 // If there's a callback then call it.
539 if ('onBeforeUpdate' in this.oCallback && typeof(this.oCallback.onBeforeUpdate) == 'string')
540 {
541 // If it returns false the item must not be added.
542 if (!eval(this.oCallback.onBeforeUpdate + '(' + this.opt.sSelf + ');'))
543 return false;
544 }
545
546 this.oRealTextHandle.value = this.oTextHandle.value;
547
548 if (isEmptyText(this.oTextHandle))
549 {
550 this.aCache = [];
551
552 this.populateDiv();
553
554 this.autoSuggestHide();
555
556 return true;
557 }
558
559 // Nothing changed?
560 if (this.oTextHandle.value == this.sLastDirtySearch)
561 return true;
562 this.sLastDirtySearch = this.oTextHandle.value;
563
564 // We're only actually interested in the last string.
565 var sSearchString = this.oTextHandle.value.replace(/^("[^"]+",[ ]*)+/, '').replace(/^([^,]+,[ ]*)+/, '');
566 if (sSearchString.substr(0, 1) == '"')
567 sSearchString = sSearchString.substr(1);
568
569 // Stop replication ASAP.
570 var sRealLastSearch = this.sLastSearch;
571 this.sLastSearch = sSearchString;
572
573 // Either nothing or we've completed a sentance.
574 if (sSearchString == '' || sSearchString.substr(sSearchString.length - 1) == '"')
575 {
576 this.populateDiv();
577 return true;
578 }
579
580 // Nothing?
581 if (sRealLastSearch == sSearchString)
582 return true;
583
584 // Too small?
585 else if (sSearchString.length < this.iMinimumSearchChars)
586 {
587 this.aCache = [];
588 this.autoSuggestHide();
589 return true;
590 }
591 else if (sSearchString.substr(0, sRealLastSearch.length) == sRealLastSearch)
592 {
593 // Instead of hitting the server again, just narrow down the results...
594 var aNewCache = [];
595 var j = 0;
596 var sLowercaseSearch = sSearchString.toLowerCase();
597 for (var k = 0; k < this.aCache.length; k++)
598 {
599 if (this.aCache[k].sItemName.substr(0, sSearchString.length).toLowerCase() == sLowercaseSearch)
600 aNewCache[j++] = this.aCache[k];
601 }
602
603 this.aCache = [];
604 if (aNewCache.length != 0)
605 {
606 this.aCache = aNewCache;
607 // Repopulate.
608 this.populateDiv(this.aCache);
609
610 // Check it can be seen.
611 this.autoSuggestShow();
612
613 return true;
614 }
615 }
616
617 // In progress means destroy!
618 if (typeof(this.oXmlRequestHandle) == 'object' && this.oXmlRequestHandle != null)
619 this.oXmlRequestHandle.abort();
620
621 // Clean the text handle.
622 sSearchString = sSearchString.php_to8bit().php_urlencode();
623
624 // Get the document.
625 this.tmpMethod = getXMLDocument;
626 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);
627 delete this.tmpMethod;
628
629 return true;
630 }