/*
* Copyright (c) 2013 Bundesamt für Kartographie und Geodäsie.
* See license.txt in the BKG WebMap distribution or repository for the
* full text of the license.
*
* Author: Dirk Thalheim
* derived from Timothy Groves - http://www.brandspankingnew.net
*/
/**
* @requires OpenLayers/BaseTypes/Class.js
* @requires OpenLayers/Util.js
* @requires BKGWebMap/Util.js
*/
/**
* @classdesc Tool zur Bereitstellung einer AutoSuggest-Funktionalität
*
* @constructor BKGWebMap.Util.AutoSuggest
* @param {node} input - Inputelement für das die Vervollständigung aktiviert werden soll
* @param {BKGWebMap.Protocol.Geoindex} protocol - Protokoll für Suche
* @param {object} options - Optionen zur Modifikation der Parameter
*/
BKGWebMap.Util.AutoSuggest = OpenLayers.Class({
/**
* Minimale Anzahl an Zeichen, ab der Vorschläge gesucht werden
* @memberOf BKGWebMap.Util.AutoSuggest
* @type int
*/
minchars : 1,
/**
* CSS-Klasse für Autocomplete
* @memberOf BKGWebMap.Util.AutoSuggest
* @type string
*/
className: "autosuggest",
/**
* Zeit in Millisekunden, ab der Suche gestartet werden soll
* @memberOf BKGWebMap.Util.AutoSuggest
* @type int
*/
delay: 500,
/**
* <code>true</code> wenn Hinweis angezeigt werden soll, falls keine Ergebnisse
* @memberOf BKGWebMap.Util.AutoSuggest
* @type boolean
*/
shownoresults: true,
/**
* Label für keine Ergebnisse
* @memberOf BKGWebMap.Util.AutoSuggest
* @type string
*/
noresults: "Keine Ergebnisse!",
/**
* Inputfeld, auf dem Autovervollständigung angewandt werden soll
* @memberOf BKGWebMap.Util.AutoSuggest
* @type node
*/
input: null,
/**
* Eventlistener für Input-Feld
* @memberOf BKGWebMap.Util.AutoSuggest
* @type OpenLayers.Events
*/
events: null,
/**
* Protokoll für Vorschlagssuche
* @memberOf BKGWebMap.Util.AutoSuggest
* @type BKGWebMap.Protocol.Geoindex
*/
protocol: null,
/**
* <code>true</code> wenn Vorschläge angezeigt werden
* @memberOf BKGWebMap.Util.AutoSuggest
* @type boolean
*/
isVisible: false,
initialize: function(input, protocol, options) {
OpenLayers.Util.extend(this, options);
this.input = OpenLayers.Util.getElement(input);
this.protocol = protocol;
// init variables
this.sInput = "";
this.suggestions = [];
this.highlighted = 0;
this.input.setAttribute("autocomplete","off");
// create holding div
this.div = document.createElement("div");
OpenLayers.Element.addClass(this.div, this.className);
OpenLayers.Element.addClass(this.className);
this.hideSuggestions();
this.registerEvents();
},
destroy: function() {
this.cancel();
if(this.divEvents) {
this.divEvents.destroy();
this.divEvents = null;
}
this.suggestions = null;
},
/**
* Events für Input und Search-Button registrieren.
* @memberOf BKGWebMap.Control.Search
* @private
*/
registerEvents: function() {
// Registriere Keyboardevents
if(!this.events) {
// Wenn noch kein Eventlistener erzeugt wurde, erzeuge Neuen mit minimalen Browserevents
this.events = new OpenLayers.Events(this, this.input, null, true, {BROWSER_EVENTS: ["keydown", "keyup"]});
} else {
// Falls Eventlistener übergeben wurde, muss ggf. noch keyup beobachtet werden
OpenLayers.Event.observe( this.input, "keyup", this.events.eventHandler );
}
this.events.registerPriority("keydown", this, this.onKeyDown);
this.events.registerPriority("keyup", this, this.onKeyUp);
// default event handling for input
var stopEvent = function(evt) {OpenLayers.Event.stop(evt, true);};
this.divEvents = new OpenLayers.Events(this, this.div, null, true);
// onchange-Event beobachten
//OpenLayers.Event.observe( this.input, "onchange", this.inputEvents.eventHandler );
this.divEvents.on({
"mousedown": stopEvent,
"mousemove": stopEvent,
"mouseup": stopEvent,
"click": stopEvent,
scope: this
});
OpenLayers.Event.observe( document.documentElement, "click", OpenLayers.Function.bindAsEventListener(this.hideSuggestions, this) );
},
/**
* Reagiert auf KeyDown-Events im Input-Feld. Ermöglicht dem Nutzer Einträge auszublenden und auszuwählen.
* <br/>
* ENTER: selektierten Vorschlag übernehmen<br/>
* ESC: Vorschläge verbergen
*
* @param {event} evt - KeyDownEvent
* @memberOf BKGWebMap.Util.AutoSuggest
*/
onKeyDown: function(evt) {
switch(evt.keyCode) {
case OpenLayers.Event.KEY_RETURN:
if(!this.visible) return true;
if(this.suggestions.length == 0 || !this.highlighted) {
this.hideSuggestions();
return false;
}
this.setHighlightedValue();
// Event wird nicht gestoppt und kann weiterverabreitet werden. Z.B. Suche starten.
return true;
case OpenLayers.Event.KEY_ESC:
this.hideSuggestions();
// keine weitere Verarbeitung des Events
OpenLayers.Event.stop(evt, true);
return true;
default:
return true;
}
},
/**
* Reafuert auf KeyUp-Events. Ermöglicht dem Nutzer durch Liste der Vorschläge zu scrollen oder die Vorschlagssuche
* durchzuführen.
*
* @param {event} evt - KeyUpEvent
* @memberOf BKGWebMap.Util.AutoSuggest
*/
onKeyUp: function(evt) {
switch(evt.keyCode) {
case OpenLayers.Event.KEY_UP:
this.showSuggestions();
this.changeHighlight(evt.keyCode);
return;
case OpenLayers.Event.KEY_DOWN:
this.showSuggestions();
this.changeHighlight(evt.keyCode);
return;
case OpenLayers.Event.KEY_RETURN:
// verhindert erneute Vorschlagssuche bei Auswahl
return;
default:
this.getSuggestions(this.input.value);
return;
}
},
/**
* Verbirgt die Vorschlagliste.
* @memberOf BKGWebMap.Util.AutoSuggest
*/
hideSuggestions: function() {
if(OpenLayers.Element.hasClass(this.div, 'hidden')) return;
OpenLayers.Element.removeClass(this.div, 'visible');
OpenLayers.Element.addClass(this.div, 'hidden');
this.visible = false;
},
/**
* Blendet die Vorschlagliste ein.
* @memberOf BKGWebMap.Util.AutoSuggest
*/
showSuggestions: function() {
if(OpenLayers.Element.hasClass(this.div, 'visible')) return;
if(!this.ul) {
this.getSuggestions(this.input.value);
return;
}
OpenLayers.Element.removeClass(this.div, 'hidden');
OpenLayers.Element.addClass(this.div, 'visible');
this.visible = true;
},
/**
* Startet die Vorschlagssuche.
*
* @param {string} val - der zu suchende Begrif.
* @memberOf BKGWebMap.Util.AutoSuggest
*/
getSuggestions: function (val) {
// Nur bei Änderungen des Suchbegriffs wird gesucht.
if (val == this.sInput)
return;
// Hat der Suchbegriff die minimale Länge? Wenn nein, dann noch keine Suche.
if (val.length < this.minchars) {
this.sInput = "";
return;
}
// stoppe evtl. laufende Anfragen
this.cancel();
// Führe Vorschlagssuche aus
this.sInput = val;
// starte Suche verzögert:
this.suggestDelayID = setTimeout(
OpenLayers.Function.bind( function() {
this.suggestDelayID = null;
this.response = this.protocol.suggest(this.sInput, {callback: this.setSuggestions, scope: this});
},
this
),
this.delay
);
},
/**
* Setzt die aktuell verfügbaren Vorschläge.
*
* @param {BKGWebMap.Protocol.Geoindex} response
* @memberOf BKGWebMap.Util.AutoSuggest
*/
setSuggestions: function (response) {
this.response = null;
this.suggestions = response.suggestions;
this.createList(this.suggestions);
this.showSuggestions();
},
/**
* Erzeugt eine HTML-Liste für die Vorschläge
* @param {Array<object>} arr - Liste der Vorschläge
* @memberOf BKGWebMap.Util.AutoSuggest
*/
createList: function(arr) {
// Lösche die alte Liste
this.div.innerHTML = '';
// Erzeuge und fülle die neue Liste
this.ul = document.createElement("ul");
// Fülle die Liste
var li;
for (var i=0;i<arr.length;i++) {
var val = arr[i];
var span = document.createElement("span");
span.innerHTML = val.label;
var a = document.createElement("a");
a.href = "#";
a.name = i+1;
a.appendChild(span);
li = document.createElement("li");
li.appendChild(a);
// registriere Mouseevents
var events = new OpenLayers.Events(this, a, null, true);
events.on({
"click": this.onSuggestionClick,
"mouseover": this.onMouseHighlight,
scope: this
});
this.ul.appendChild(li);
}
// no results
if (arr.length == 0) {
li = document.createElement("li");
OpenLayers.Element.addClass("as_warning");
li.appendChild(document.createTextNode(this.noresults));
this.ul.appendChild( li );
}
this.div.appendChild( this.ul );
this.input.parentNode.insertBefore(this.div, this.input.nextSibling);
// currently no item is highlighted
this.highlighted = 0;
},
/**
* Ändert den ausgewählten Vorschlag per Cursortaste.
*
* @param {int} key - Keycode
* @memberOf BKGWebMap.Util.AutoSuggest
*/
changeHighlight: function(key) {
if (!this.ul || this.suggestions.length == 0) return;
var n=0;
if (key == OpenLayers.Event.KEY_DOWN)
n = this.highlighted + 1;
else if (key == OpenLayers.Event.KEY_UP)
n = this.highlighted - 1;
// gehe an den Anfang der Liste, wenn am Ende
if (n > this.ul.childNodes.length) n = 1;
// deaktiviere Auswahl und stelle
if (n < 1) {
n = 0;
this.input.value = this.sInput;
}
this.setHighlight(n);
},
/**
* Event-Handler für Clicks auf Einträge der Vorschlagsliste. Wird auf KeyDown-Enter weitergeleitet.
* @param {Event} evt - Der Click-Event
* @memberOf BKGWebMap.Util.AutoSuggest
*/
onSuggestionClick: function(evt) {
this.events.triggerEvent('keydown', {keyCode: OpenLayers.Event.KEY_RETURN});
},
/**
* Ändert den ausgewählten Vorschlag per Mouseover.
*
* @param {event} evt - Mouseover Event
* @memberOf BKGWebMap.Util.AutoSuggest
*/
onMouseHighlight: function(evt) {
// hole das Listenelement für den Mousover Event
var li = BKGWebMap.Util.getEventTarget(evt);
while( li.tagName.toLowerCase() != 'li')
li = li.parentNode;
this.setHighlight( BKGWebMap.Util.getIndex(li) + 1 );
OpenLayers.Event.stop(evt, true);
return false;
},
/**
* Setzt den hervorgehobenen Vorschlag
* @param {int} n - Index des ausgewählten Vorschlag
* @memberOf BKGWebMap.Util.AutoSuggest
*/
setHighlight: function(n) {
if (!this.ul || this.suggestions.length == 0) return;
if(this.highlighted == n)
return;
this.clearHighlight();
this.highlighted = n;
if(this.highlighted == 0) return;
var li = this.ul.childNodes[this.highlighted-1];
OpenLayers.Element.addClass(li, "highlight");
},
/**
* Löscht die Hervorhebungen in der Vorschlagsliste.
* @memberOf BKGWebMap.Util.AutoSuggest
*/
clearHighlight: function() {
if (!this.ul) return;
if (this.highlighted > 0) {
OpenLayers.Element.removeClass(this.ul.childNodes[this.highlighted-1], "highlight");
this.highlighted = 0;
}
},
/**
* Übernimmt den hervorgehobenen Vorschlag als Suchbegriff
* @memberOf BKGWebMap.Util.AutoSuggest
*/
setHighlightedValue: function () {
if (!this.highlighted) {
return;
}
this.sInput = this.input.value = this.suggestions[ this.highlighted - 1 ].value;
// Setze Cursor ans Ende (safari)
this.input.focus();
if (this.input.selectionStart)
this.input.setSelectionRange(this.sInput.length, this.sInput.length);
this.clearSuggestions();
},
/**
* Leert die Vorschlagliste
* @memberOf BKGWebMap.Util.AutoSuggest
*/
clearSuggestions: function () {
this.hideSuggestions();
this.div.innerHTML = '';
this.ul = null;
this.suggestions = [];
//this.sInput = '';
},
/**
* Stoppt evtl. laufende Anfragen
* @memberOf BKGWebMap.Util.AutoSuggest
*/
cancel: function() {
if(this.suggestDelayID) clearTimeout(this.suggestDelayID);
if(this.response) this.protocol.abort(this.response);
},
CLASS_NAME: "BKGWebMap.Util.AutoSuggest"
});