Indeks, wyszukiwarka

Wstęp

Przydatną funkcją na każdej stronie jest jakaś forma wyszukiwarki. Jej rola jest nie do przecenienia. Dzięki wyszukiwarce użytkownicy, którzy nie znają dobrze serwisu, mogą w mig odnaleźć interesujące ich informacje. Przedzieranie się przez spis treści nie każdemu odpowiada, a poza tym nie wszystko można tam odnaleźć.

Specjalną formą wyszukiwarki jest indeks lub inaczej skorowidz. Można go znaleźć w niemal każdym podręczniku. "Papierowa" wersja skorowidza jednak nie do końca jest wygodna, ponieważ nadal zmusza do wertowania stron i szukania haseł według kolejności liter w alfabecie. Na szczęście komputer może to zrobić za nas. Wystarczy, że użytkownik zacznie wpisywać żądane hasło, a w miarę jak będzie to robił, odszukiwane będą pozycje coraz bardziej pasujące do zapytania. Dodatkowo skoro będziemy już mieli gotowy indeks, nic nie stoi na przeszkodzie, aby zbudować z niego normalną wyszukiwarkę. Dzięki temu będzie można odszukać hasła, które nie tylko rozpoczynają się od wpisywanych liter, ale również mogą figurować wewnątrz haseł zapisanych w skorowidzu. Taka funkcjonalność znacznie zwiększa możliwości.

Warto dodać, że taki indeks wcale nie musi być alternatywą dla tradycyjnych wyszukiwarek. Może bardzo dobrze je uzupełniać, ponieważ oferuje inną funkcjonalność, dzięki której wyniki zwykle będą bardziej trafne, ponieważ hasła do indeksu dodaje człowiek, a nie komputer.

Przykład

Wyszukiwarka

Skrypt

Wymagana wiedza:

Aby wstawić na swoją stronę indeks z dodatkową funkcją wyszukiwarki, postępują według poniższych instrukcji:

Utwórz w swoim edytorze HTML nowy dokument. Wklej do niego poniższy kod i zapisz w pliku indeks.js (koniecznie użyj kodowania utf-8):

/**
 * @author Sławomir Kokłowski {@link http://www.kurshtml.edu.pl}
 * @copyright NIE usuwaj tego komentarza! (Do NOT remove this comment!)
 */

function Indeks(id, ramka)
{
	this.id = id;
	if (typeof ramka == 'undefined') this.ramka = self;
	else if (typeof ramka == 'string')
	{
		if (ramka == '_blank') this.ramka = '';
		else if (ramka == '_self') this.ramka = self;
		else if (ramka == '_parent') this.ramka = parent;
		else if (ramka == '_top') this.ramka = top;
		else this.ramka = ramka;
	}
	else if (!ramka) this.ramka = self;
	else this.ramka = ramka;
	
	this._szukaj = {
		html: '',
		haslo: '',
		i: 0
	};
}

Indeks.prototype.wstaw = function(hasla, adres_bazowy, rozmiar, sortuj)
{
	if (typeof sortuj != 'undefined' && sortuj)
	{
		hasla.sort(
			function(a, b)
			{
				if (a[0] == b[0]) return 0;
				return a[0].compare() < b[0].compare() ? -1 : 1;
			}
		);
	}
	if (typeof sortuj != 'undefined' && sortuj < 0)
	{
		document.write("<pre>");
		for (var i = 0; i < hasla.length; i++)
		{
			document.write("['" + hasla[i][0].addSlashes() + "','" + hasla[i][1].addSlashes() + "']" + (i < hasla.length - 1 ? ",\n" : ""));
		}
		document.write("</pre>");
	}
	else
	{
		document.write(
			'<form id="' + this.id + '" action="javascript:void(0)" onsubmit="' + this.id + '.wyswietl(); return false">' +
				'<input type="text" name="haslo" size="30" onkeyup="' + this.id + '.zaznacz()" class="text">' +
				'<div><select name="hasla" size="' + (typeof rozmiar != 'undefined' && rozmiar ? rozmiar : 15) + '" ondblclick="' + this.id + '.wyswietl()">'
		);
		for (var i = 0; i < hasla.length; i++)
		{
			document.write('<option value="' + ((typeof adres_bazowy == 'undefined' || !adres_bazowy ? '' : adres_bazowy) + hasla[i][1]).htmlSpecialChars(true) + '"' + (i == 0 ? ' selected' : '') + '>' + hasla[i][0].htmlSpecialChars() + '</option>');
		}
		document.write(
				'</select></div>' +
				'<input type="submit" value="Wyświetl" class="button" onclick="this.form.submit()"> <input type="button" value="Szukaj" onclick="' + this.id + '.szukaj()" class="button">' +
			'</form>' +
			'<div id="' + this.id + '_szukaj"></div>'
		);
		
		var matches = window.location.search.match(new RegExp('[?&]' + this.id + '=([^&]+)'));
		if (matches && typeof matches[1] != 'undefined')
		{
			document.forms[this.id].elements['haslo'].value = unescape(matches[1]);
			this.zaznacz();
			this.szukaj();
		}
	}
}

Indeks.prototype.wstawWyszukiwarke = function(adres)
{
	document.write(
		'<form action="' + adres.htmlSpecialChars(true) + '" method="get" onsubmit="' + this.id + '.wyszukaj(this.action, this.elements[0].value); return false">' +
			'<input type="text" class="text">' +
			'<input type="submit" value="Szukaj" class="button">' +
		'</form>'
	);
}

Indeks.prototype.wyszukaj = function(adres, haslo)
{
	var search = adres.match(/#.*/);
	adres = adres.replace(/#.*/, '').replace(new RegExp('[&?]+' + this.id + '=[^&]*', 'g'), '');
	if (adres.indexOf('&') >= 0 && adres.indexOf('?') < 0) adres = adres.replace(/&/, '?');
	this.wyswietl(adres + (adres.indexOf('?') < 0 ? '?' : '&') + this.id + '=' + escape(haslo) + (search ? search : ''));
}

Indeks.prototype.wyswietl = function(adres)
{
	if (typeof adres == 'undefined') adres = document.forms[this.id].elements['hasla'].value;
	if (typeof this.ramka == 'string')
	{
		var okno = window.open(adres, this.ramka, 'menubar=yes,toolbar=yes,location=yes,directories=no,status=yes,scrollbars=yes,resizable=yes');
		if (okno) okno.focus();
		else window.location.href = adres;
	}
	else this.ramka.location.href = adres;
}

Indeks.prototype.zaznacz = function()
{
	var haslo = document.forms[this.id].elements['haslo'].value.toLowerCase();
	for (var i = 0; i < document.forms[this.id].elements['hasla'].options.length; i++)
	{
		if (document.forms[this.id].elements['hasla'].options[i].text.toLowerCase().indexOf(haslo) == 0)
		{
			document.forms[this.id].elements['hasla'].options[i].selected = true;
			break;
		}
	}
}

Indeks.prototype.szukaj = function()
{
	if (!this._szukaj.i)
	{
		if (document.forms[this.id].elements['haslo'].value == '') return;
		var haslo = document.forms[this.id].elements['haslo'].value.toLowerCase().replace(/[^a-ząćęłńóśźż_*?"]+/gi, ' ').replace(/\*/g, '[a-ząćęłńóśźż_]*').replace(/\?/g, '[a-ząćęłńóśźż_]');
		this._szukaj.haslo = haslo.match(/"[^"]+"/g);
		if (!this._szukaj.haslo) this._szukaj.haslo = new Array();
		else
		{
			for (var i = 0; i < this._szukaj.haslo.length; i++)
			{
				var pos = -1;
				var len = this._szukaj.haslo[i].length;
				while ((pos = haslo.indexOf(this._szukaj.haslo[i], pos + 1)) >= 0)
				{
					haslo = haslo.substring(0, pos) + haslo.substring(pos + len);
				}
				this._szukaj.haslo[i] = this._szukaj.haslo[i].replace(/(^[" ]+|[" ]+$)/g, '');
			}
		}
		haslo = haslo.replace(/[" ]+/g, ' ').replace(/(^ | $)/g, '');
		if (haslo != '' && haslo != ' ')
		{
			haslo = haslo.split(' ');
			for (var i = 0; i < haslo.length; i++)
			{
				this._szukaj.haslo[this._szukaj.haslo.length] = haslo[i];
			}
		}
		var oHaslo = new Object();
		for (var i = 0; i < this._szukaj.haslo.length; i++)
		{
			oHaslo[this._szukaj.haslo[i]] = this._szukaj.haslo[i];
		}
		this._szukaj.haslo = new Array();
		for (var haslo in oHaslo)
		{
			if (haslo.length > 2) this._szukaj.haslo[this._szukaj.haslo.length] = new RegExp('(^|[^a-ząćęłńóśźż_])' + haslo + '([^a-ząćęłńóśźż_]|$)');
		}
		this._szukaj.html = '';
		if (this._szukaj.haslo.length == 0) return;
	}
	for (; this._szukaj.i < document.forms[this.id].elements['hasla'].options.length; this._szukaj.i++)
	{
		for (var j = 0; j < this._szukaj.haslo.length; j++)
		{
			if (document.forms[this.id].elements['hasla'].options[this._szukaj.i].text.toLowerCase().search(this._szukaj.haslo[j]) >= 0)
			{
				var tag = new Array('<a href="' + document.forms[this.id].elements['hasla'].options[this._szukaj.i].value.htmlSpecialChars(true) + '"' + (this.ramka ? ' onclick="' + this.id + '.wyswietl(this.href); return false"' : '') + '>', '</a>');
				var text = document.forms[this.id].elements['hasla'].options[this._szukaj.i].text.htmlSpecialChars();
				var pos = this._szukaj.html.indexOf(tag[0]);
				if (pos >= 0)
				{
					var pos = this._szukaj.html.indexOf(tag[1], pos);
					this._szukaj.html = this._szukaj.html.substring(0, pos) + ',<br>' + text + this._szukaj.html.substring(pos);
				}
				else this._szukaj.html += '<li>' + tag[0] + text + tag[1] + '</li>';
				break;
			}
		}
		if (this._szukaj.i && this._szukaj.i < document.forms[this.id].elements['hasla'].options.length - 1 && !(this._szukaj.i % 100))
		{
			document.getElementById(this.id + '_szukaj').innerHTML = Math.floor(this._szukaj.i / document.forms[this.id].elements['hasla'].options.length * 100) + '%';
			this._szukaj.i++;
			setTimeout(this.id + '.szukaj()', 1);
			return;
		}
	}
	document.getElementById(this.id + '_szukaj').innerHTML = '<p>Wyniki wyszukiwania: <em>' + document.forms[this.id].elements['haslo'].value.htmlSpecialChars() + '</em></p>' + (this._szukaj.html == '' ? '' : '<ol>' + this._szukaj.html + '</ol>');
	this._szukaj.i = 0;
}


String.prototype.htmlSpecialChars = function(attribute)
{
	var str = this.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
	if (typeof attribute != 'undefined' && !attribute) str = str.replace(/"/g, '&quot;');
	return str;
}
String.prototype.addSlashes = function(limiter)
{
	if (typeof limiter == 'undefined') limiter = "'";
	var regExp = new RegExp(limiter, 'g');
	return this.replace(/\\/g, '\\\\').replace(regExp, '\\' + limiter);
}
if (!String.prototype._toLowerCase)
{
	String.prototype._toLowerCase = String.prototype.toLowerCase;
	String.prototype.toLowerCase = function()
	{
		return this._toLowerCase().replace(/[ĄĆĘŁŃÓŚŹŻ]/g,
			function(str)
			{
				if (str == 'Ą') return 'ą';
				if (str == 'Ć') return 'ć';
				if (str == 'Ę') return 'ę';
				if (str == 'Ł') return 'ł';
				if (str == 'Ń') return 'ń';
				if (str == 'Ó') return 'ó';
				if (str == 'Ś') return 'ś';
				if (str == 'Ź') return 'ź';
				if (str == 'Ż') return 'ż';
				return str;
			}
		);
	}
}
String.prototype.compare = function()
{
	return this.toLowerCase().replace(/[ąćęłńóśźż]/g,
		function(str)
		{
			if (str == 'ą') return 'aż';
			if (str == 'ć') return 'cż';
			if (str == 'ę') return 'eż';
			if (str == 'ł') return 'lż';
			if (str == 'ń') return 'nż';
			if (str == 'ó') return 'oż';
			if (str == 'ś') return 'sż';
			if (str == 'ź') return 'zż';
			if (str == 'ż') return 'zżż';
			return str;
		}
	);
}

Następnie w taki sam sposób utwórz drugi plik - tym razem pod nazwą indeks_hasla.js - i wklej w nim (znowu należy koniecznie pamiętać o ustawieniu w edytorze kodowania znaków utf-8):

var indeks = new Indeks('indeks');
indeks.wstaw(new Array(
['Hasło 1','adres1'],
['Hasło 2','adres2'],
['Hasło 3','adres3']
));
Przy czym w wyróżnionych fragmentach należy wpisać.
indeks
Identyfikator indeksu. Może zawierać tylko angielskie litery, cyfry (ale nie na początku) i podkreślniki ("_"). W jednym dokumencie nie mogą istnieć dwa indeksy, ani żadne inne elementy, o takim samym identyfikatorze!
Hasło 1, Hasło 2, Hasło 3
Tekst haseł. Nie może zawierać znaków: ' ani \. Jeżeli takie znaki muszą się w nim znaleźć, należy je poprzedzić odwróconym ukośnikiem: \' i \\.
adres1, adres2, adres3
Lokalizacja dokumentów zawierających podane hasła.

Hasła powinny być podane w kolejności alfabetycznej.

Zauważ, że po każdym kolejnym wierszu z hasłami indeksu (oprócz ostatniego!) znajduje się przecinek.

Na koniec wystarczy otworzyć dokument HTML, w których chcemy wstawić indeks i w jego nagłówku (<head>...</head>) wkleić jeden raz następujący kod:

<script src="indeks.js"></script>

W tym samym dokumencie HTML, ale już w wybranym miejscu strony, trzeba jeszcze wstawić:

<script src="indeks_hasla.js"></script>

...i już można cieszyć się indeksem/wyszukiwarką na swojej stronie :-)

Wieloznacznik (wildcard)

Wieloznacznik (ang. wildcard) to symbol stosowany przy wyszukiwaniu tekstu, który zastępuje pewną grupę znaków. Wyszukiwarka w indeksie obsługuje dwa takie symbole:

*
zastępuje dowolny fragment wyrazu (również pusty)
?
zastępuję dokładnie jedną literę

Wieloznaczniki są przydatne, jeśli znamy tylko pewną część wyrazu, co zwykle ma miejsce, kiedy chcemy odszukać wszystkie odmiany wybranego słowa. Dzięki temu po wpisaniu języ*, odnalezione mogą być hasła, które zawierają wyrazy: język, językiem, języku, języczek itd. Natomiast języ? pasuje tylko do język. Wpisanie wyraz*, spowoduje odszukanie zarówno po prostu słowa wyraz, jak i np. wyrazy, ale już zapis wyraz? nie obejmuje słowa wyraz. Symbole wieloznaczników można wykorzystywać w dowolnym miejscu szukanych wyrazów, nie tylko na końcu, np. *języ* pasuje zarówno do słowa język, jak i wielojęzyczny.

Adres bazowy

Czasami może zdarzyć się tak, że wszystkie adresy do haseł z indeksu rozpoczynają się tak samo. Oczywiście w takim przypadku ten sam fragment ścieżki można dodawać w każdym kolejnym haśle, ale można również zrobić to tylko raz. W tym celu należy zmodyfikować wpis w pliku indeks_hasla.js:

var indeks = new Indeks('indeks');
indeks.wstaw(new Array(
['Hasło 1','adres1'],
['Hasło 2','adres2'],
['Hasło 3','adres3']
), 'adres_bazowy');

Teraz nie trzeba już wpisywać tego prefiksu w adresach na liście haseł.

Rozmiar

Jeżeli zachodzi potrzeba zmiany wysokości okienka z hasłami, należy zmodyfikować wpis w pliku indeks_hasla.js:

var indeks = new Indeks('indeks');
indeks.wstaw(new Array(
['Hasło 1','adres1'],
['Hasło 2','adres2'],
['Hasło 3','adres3']
), null, rozmiar);
Rozmiar podaje wysokość okienka z hasłami wyrażoną w liczbie linii, które się w nim zmieszczą bez konieczności przewijania.

Automatyczne sortowanie

Jak już wspomniono, dla poprawnego działania indeksu konieczne jest, aby hasła na liście były ułożone w kolejności alfabetycznej. Można ustawić specjalną opcję, tak aby sortowanie alfabetyczne odbywało się automatycznie:

var indeks = new Indeks('indeks');
indeks.wstaw(new Array(
['Hasło 1','adres1'],
['Hasło 2','adres2'],
['Hasło 3','adres3']
), null, null, true);

Ustawienie tej opcji może znacznie wydłużyć generowanie indeksu!

Aby wyeliminować tę niedogodność, a jednocześnie nie musieć przy każdym dodawaniu haseł do indeksu żmudnie układać je według alfabetu, można testowo użyć takiego kodu:

var indeks = new Indeks('indeks');
indeks.wstaw(new Array(
['Hasło 1','adres1'],
['Hasło 2','adres2'],
['Hasło 3','adres3']
), null, null, -1);

Po jego wpisaniu, na stronie nie zostanie wyświetlony normalny indeks, ale fragment kodu skryptu z hasłami ułożonymi alfabetycznie. Wystarczy ten kod skopiować, wkleić w odpowiednie miejsce do pliku indeks_hasla.js i na koniec usunąć wartość opcji (wraz z końcowym przecinkiem przed zamknięciem nawiasu).

Łączenie opcji

Wszystkie z przedstawionych powyżej opcji można łączyć, przy czym skrajane argumenty z prawej strony, których nie chcemy podawać, można po prostu pominąć. Dla pozostałych, które chcemy pominąć, wystarczy wpisać null (ale nie 'null'!), np.:

var indeks = new Indeks('indeks');
indeks.wstaw(new Array(
['Hasło 1','adres1'],
['Hasło 2','adres2'],
['Hasło 3','adres3']
), null, 10);

Ramka

Jeżeli Twój serwis wykorzystuje ramki, a jednocześnie dokumenty, do których odsyłają poszczególne hasła, mają się wyświetlać w innej ramce (np. indeks znajduje się w ramce spisu treści, a wyniki mają być wczytywane w ramce głównej - z treścią), trzeba będzie podać ramkę, do której mają być wczytywane dokumenty z indeksu. Należy to zrobić w pliku indeks_hasla.js:

var indeks = new Indeks('indeks', ramka);
indeks.wstaw(new Array(
['Hasło 1','adres1'],
['Hasło 2','adres2'],
['Hasło 3','adres3']
));
Możliwe wartości:
self
Wartość domyślna - wczytanie nastąpi do aktualnej ramki. Jest to przydatne w następujących przypadkach:
  • serwis w ogóle nie składa się z ramek (wtedy w ogóle można pominąć ten argument albo wpisać null)
  • strona z indeksem znajduje się w tej samej ramce, do której chcemy wczytać wynik
self.nazwaramki
Na stronie z indeksem znajduje się ramka lokalna o nazwie nazwaramki, do której ma zostać wczytany wynik. Nazwa ramki może zawierać tylko angielskie litery, cyfry (ale nie na początku) i podkreślniki ("_"). [Zobacz: Wczytywanie strony do ramki lokalnej]
parent.nazwaramki
Stosujemy, gdy serwis składa się z ramek, ale jest tylko jeden plik definiujący ich strukturę albo ewentualnie jest kilka zagnieżdżonych struktur zapisanych w różnych plikach, jednak wyniki chcemy wczytać do ramki bezpośrednio nadrzędnej
top.nazwaramki
Serwis składa się z ramek i chcemy wczytać wyniki do ramki najbardziej nadrzędnej

Nowe okno

Aby otworzyć stronę w nowym oknie, należy zmienić plik indeks_hasla.js następująco:

var indeks = new Indeks('indeks', 'nazwaokna');
indeks.wstaw(new Array(
['Hasło 1','adres1'],
['Hasło 2','adres2'],
['Hasło 3','adres3']
));
Jako nazwaokna można wpisać:
_blank
Wyniki będą za każdym razem otwierane w nowym oknie
_self
Odpowiednik self z poprzedniego punktu
_parent
Odpowiednik parent z poprzedniego punktu
_top
Odpowiednik top z poprzedniego punktu
nazwa
Wyniki będą otwierane w nowym, ale zawsze tym samym oknie. Innymi słowy: nigdy nie zostaną otwarte dwa okna z wynikami tego samego indeksu. Nazwa może zawierać tylko angielskie litery, cyfry (ale nie na początku) i podkreślniki ("_").

Wygląd

Jeżeli nie odpowiada nam podstawowy wygląd indeksu, można go zmienić wykorzystując polecenia CSS. W tym celu w pliku indeks.css należy wkleić np.:

#indeks {
	width: 300px;
	padding: 10px 0;
	background: #ccc;
	border: 2px outset #aaa;
	text-align: center;
	margin: 1em auto;
}

#indeks input.text {
	width: 290px;
}

#indeks select {
	width: 100%;
	margin: 2px 0 10px 0;
}

#indeks input.button {
	width: 100px;
	margin-left: 5px;
	margin-right: 5px;
}

W wyróżnionych miejscach wpisano identyfikator indeksu, zdefiniowany uprzednio w pliku indeks_hasla.js. Teraz w nagłówku dokumentu HTML (<head>...</head>), na którym wstawiony jest indeks haseł, wystarczy wstawić odwołanie do utworzonego właśnie zewnętrznego arkusza stylów:

<link rel="Stylesheet" href="indeks.css">

Wyszukiwarka

Bardzo pomocne dla użytkowników może okazać się wstawienie formularza wyszukiwania na wszystkich stronach serwisu. Aby to zrobić, należy wstawić w nagłówku strony (<head>...</head>) odwołanie do pliku indeks.js:

<script src="indeks.js"></script>

Nie trzeba natomiast wstawiać odwołania do skryptu indeks_hasla.js, w którym znajduje się lista haseł. Następnie w wybranym miejscu strony wystarczy wkleić:

<script>
var indeks = new Indeks('indeks');
indeks.wstawWyszukiwarke('adres');
</script>
Godne podkreślenia są wyróżnione wartości:
indeks
Identyfikator indeksu - musi być identyczny, jak podany w pliku indeks_hasla.js
adres
Lokalizacja dokumentu HTML, w którym został umieszczony indeks z hasłami

Można również podać nazwę ramki lub wymusić otwieranie wyników z wyszukiwarki w nowym oknie - dokładnie jak w pliku indeks_hasla.js:

<script>
var indeks = new Indeks('indeks', ramka);
indeks.wstawWyszukiwarke('adres');
</script>
lub
<script>
var indeks = new Indeks('indeks', 'nazwaokna');
indeks.wstawWyszukiwarke('adres');
</script>

Przykład:

Demonstrację działania wyszukiwarki można obejrzeć w części Przykład tego rozdziału. Zwróć uwagę, że po wpisaniu hasła i kliknięciu przycisku "Szukaj", nastąpi wczytanie strony z indeksem, w którym automatycznie uruchomi się proces wyszukiwania wpisanych wyrazów.