Podstawy#

Zestawy#

Zestawy (sets) są obiektową reprezentacją dla jakiś elementów, np. unikatowych słów # (unique tokens), które najczęściej generuje się na podstawie łańcucha znakowego. Każde unikatowe słowo będzie osobnym elementem zestawu. Obiekty zestawów implementują konkretne interfejsy, które ułatwiają manipulowanie elementami zestawów. W pewnych sytuacjach takie rozwiązanie jest wygodniejsze, niż modyfikacja czystego łańcucha znakowego.

Interfejsy i polecenia#

W specyfikacji DOM4 zestawy zostały całkowicie przedefiniowane. Obecnie wyrażane są za pomocą jednego bazowego interfejsu DOMTokenList. Przez pewien czas w specyfikacji DOM4 istniał jeszcze dziedziczący po nim interfejs DOMSettableTokenList, ale w ramach uproszczenia platformy webowej został on całkowicie usunięty i całą jego funkcjonalność, czyli m.in. automatyczne przekierowanie na inną właściwość przy ustawianiu, przeniesiono wprost do interfejsu DOMTokenList (DOM - Bug 119).

Nazwy "DOMTokenList" i "DOMSettableTokenList" pozostawiono ze względów historycznych. Ich zmiana mogłaby zakłócić kompatybilność z już istniejącymi implementacjami i specyfikacjami (np. HTML5). W związku z tym zestawy bardzo często nazywa się po prostu listami, ale nie jest to prawidłowe, bo listy z definicji zezwalają na pojawianie się duplikatów.

W poprzedniej specyfikacji DOM Level 3 Core zestawy były charakteryzowane przez interfejs DOMStringList, który został usunięty w aktualnej specyfikacji DOM4.

Poniższa tabela zawiera wykaz wszystkich właściwości operujących na zestawach słów DOM, odpowiadający im atrybut oraz ewentualne obsługiwane słowa:

Właściwość IDLOdpowiadający atrybutObsługiwane słowa
DOM4
Element.classListclassBrak
HTML5
HTMLLinkElement.sizessizesBrak
HTMLOutputElement.htmlForforBrak
HTMLElement.dropzonedropzoneDefinicja: "copy", "move", "link", "string:" i "file:"
HTMLIFrameElement.sandboxsandboxDefinicja: allow-forms, allow-modals, allow-pointer-lock, allow-popups, allow-popups-to-escape-sandbox, allow-same-origin, allow-scripts i allow-top-navigation
HTMLLinkElement.relListrelDefinicja: alternate, icon, pingback, prefetch, stylesheet i next
HTMLAnchorElement.relList
HTMLAreaElement.relList
relDefinicja: noreferrer, noopener i prefetch

Powyższy wykaz należy potraktować z dużą dozą ostrożności. W rzeczywistości przeglądarki internetowe działają odmiennie i w przypadku niektórych właściwości operują na czystym łańcuchu znakowym zamiast na zestawie słów DOM lub nie obsługują jej kompletnie. Prawidłowa implementacja wszystkich poleceń zajmie zapewne sporo czasu.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<script>

	var el_a = document.createElement("a");
	var el_area = document.createElement("area");
	var el_link = document.createElement("link");
	var el_iframe = document.createElement("iframe");
	var el_output = document.createElement("output");
	var el_html = document.documentElement;

	function supportSet(desc, el, attr){

		var status = "<span style='color: red'>ŹLE</span>";
		document.write(desc + "<br>");

		if (el[attr] != undefined){
			document.write(el.localName + "." + attr + ".constructor: " + el[attr].constructor + "<br>");
			if (typeof el[attr] == "object"){
				status = "<span style='color: green'>DOBRZE</span>";
			}
		}
		else{
			document.write("Całkowity brak obsługi tego atrybutu w elemencie." + "<br>");
		}

		document.write(status + "<br><br>");

	}

	var desc_start = "Testy obsługi obiektu DOMTokenList dla elementu ";

	supportSet(desc_start + "HTML i atrybutu class:", el_html, "classList");
	supportSet(desc_start + "HTML i atrybutu DROPZONE:", el_html, "dropzone");
	supportSet(desc_start + "LINK i atrybutu SIZES:", el_link, "sizes");
	supportSet(desc_start + "OUTPUT i atrybutu FOR:", el_output, "htmlFor");
	supportSet(desc_start + "IFRAME i atrybutu SANDBOX:", el_iframe, "sandbox");
	supportSet(desc_start + "LINK i atrybutu REL:", el_link, "relList");
	supportSet(desc_start + "A i atrybutu REL:", el_a, "relList");
	supportSet(desc_start + "AREA i atrybutu REL:", el_area, "relList");

</script>

Aktualność zestawów#

Zestawy są aktualne/żywe # (live) względem skojarzonego ze sobą atrybutu i elementu. Synchronizacja między nimi działa w obie strony i można ją scharakteryzować następująco:

Takie sprzężenie między zestawem i jego atrybutem to ukłon w stronę poprzedniego podejścia, kiedy to nie było innej możliwości modyfikowania wartości w atrybucie (np. klas w elementach), jak poprzez łańcuch znakowy. Dwukierunkowa synchronizacja zapewni większą kompatybilność wsteczną z utworzonym do tej pory kodem i w niektórych przypadkach może być wygodniejsza w użyciu, np. szybciej ustawimy kilka słów w zestawie poprzez przypisanie nowego łańcucha znakowego dla atrybutu aniżeli mielibyśmy kilkukrotnie wywoływać metodę DOMTokenList.add() dla każdego słowa z osobna.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<script>

	function showTokens(set){
		return Array.prototype.slice.call(set).join(" ");
	}

	var html = document.documentElement; // referencja do elementu html
	var htmlList = html.classList; // referencja do zestawu>

	document.write(htmlList.constructor); // function DOMTokenList() { [native code] }
	document.write("<br>");
	document.write(htmlList.length); // 0 - zestaw jest pusty

	document.write("<br><br>");

	html.className = "Test1 Test2 Test3"; // ustawiamy wartość atrybutu class

	document.write(showTokens(htmlList)); // Test1 Test2 Test3 - zestaw został automatycznie uaktualniony
	document.write("<br>");
	document.write(htmlList.length); // 3
	document.write("<br>");
	document.write(html.className); // Test1 Test2 Test3

	document.write("<br><br>");

	html.className = "Test1"; // zmieniamy wartość atrybutu class>

	document.write(showTokens(htmlList)); // Test1 - zestaw został automatycznie uaktualniony
	document.write("<br>");
	document.write(htmlList.length); // 1
	document.write("<br>");
	document.write(html.className); // Test1

	document.write("<br><br>");

	htmlList.add("Test2", "Test3");  // dodajemy dwa nowe słowa do zestawu

	document.write(showTokens(htmlList)); // Test1 Test2 Test3
	document.write("<br>");
	document.write(htmlList.length); // 3
	document.write("<br>");
	document.write(html.className); // Test1 Test2 Test3 - atrybut został automatycznie uaktualniony

</script>

Unikatowość i uporządkowanie elementów w zestawach#

Z zestawami będą związane takie pojęcia jak unikatowość # (unique) i uporządkowanie # (ordered) oraz kilka specyficznych zachowań. Najlepiej wyjaśnić wszystko na przykładzie klas elementów ze względu na ich najczęstsze użycie.

Załóżmy, że mamy do czynienia z elementem div, który w strukturze znacznikowej ma zdefiniowane za pomocą atrybutu HTML następujące klasy: class="a a c" (alternatywnie można użyć właściwości Element.className po stronie JS). Jest to jak najbardziej prawidłowe, chociaż dublowanie klas nie ma najmniejszego sensu. W tym przypadku tak naprawdę operujemy na łańcuchu znakowym, dlatego uwzględniane będą wszystkie znaki, nawet zwielokrotnione spacje.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<script>

	var div = document.createElement("div"); // referencja do kontenera DIV

	document.write(div.className); // "" - pusty łańcuch (brak zdefiniowanych klas)
	document.write("<br>");
	document.write(div.className.length); // 0

	document.write("<br><br>");

	div.className = "a a c"; // ustawiamy wartość atrybutu class

	document.write(div.className); // "a a c"
	document.write("<br>");
	document.write(div.className.length); // 5

	document.write("<br><br>");

	div.className = "   a a c   "; // dodajemy trzy spacje na początku i na końcu

	document.write(div.className); // "   a a c   "
	document.write("<br>");
	document.write(div.className.length); // 11

</script>

Takie zachowanie jest zgodne z aktualnymi standardami i utrzymywane ze względu na kompatybilność wsteczną ze starszymi implementacjami. Zestawy są czymś nowym w DOM dlatego względem nich wprowadzono nieco modyfikacji. Znowu najłatwiej wyjaśnić wszystko na podstawie klas elementów.

Zestaw z klasami możemy pobrać za pomocą właściwości Element.classList, która zwróci nam obiekt typu DOMTokenList. Obiekt ten jest automatycznie tworzony i kojarzony z uporządkowanym zestawem słów, który sam jest tworzony na podstawie klas elementów, czyli tym co aktualnie znajduje się w atrybucie class="" lub właściwości Element.className, ale eliminuje wszystkie duplikaty oraz niepotrzebne spacje. Etap tworzenia bardzo dobrze opisuje algorytm parsowania uporządkowanego zestawu. Algorytm ten może być wykorzystywany w wielu innych algorytmach, np. dla listy elementów z nazwami klas classNames.

W tym momencie elementy (unikatowe słowa) w zestawie mogą się różnić z tym, co znajduje się w atrybucie class="". Dodawanie nowych klas do atrybutu (a raczej ustawianie nowej wartości tekstowej) będzie odzwierciedlane w zestawie, ale z zachowaniem unikatowości słów.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<script>

	function showTokens(set){
		return Array.prototype.slice.call(set).join(" ");
	}

	var div = document.createElement("div"); // referencja do kontenera DIV

	div.className = "a a c"; // ustawiamy wartość atrybutu class

	document.write(div.className); // "a a c"
	document.write("<br>");
	document.write(showTokens(div.classList)); // "a c"

	document.write("<br><br>");

	div.className += " c"; // dodajemy klasę c

	document.write(div.className); // "a a c c"
	document.write("<br>");
	document.write(showTokens(div.classList)); // "a c"

</script>

Sytuacja ulega zmianie w przypadku korzystania z metod zestawów, takich jak DOMTokenList.add(), DOMTokenList.remove() czy DOMTokenList.toggle(). Wywołanie tych metod może zmodyfikować powiązany z zestawem uporządkowany zestaw słów, i dodatkowo zawsze uaktualni # zawartość skojarzonego atrybutu class="", a jeśli atrybut ten nie istniał, to najpierw zostanie utworzony (nawet z pustą wartością). Wynika to z kroków aktualizacji umieszczanych w algorytmach dla tych metod. W konsekwencji nasz oryginalny łańcuch znakowy (z ewentualnymi powtarzającymi się klasami i spacjami) zostanie zmodyfikowany przy użyciu algorytmu serializacji uporządkowanego zestawu.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<script>

	function showTokens(set){
		return Array.prototype.slice.call(set).join(" ");
	}

	var div = document.createElement("div"); // referencja do kontenera DIV
	div.className = "   a a c   "; // ustawiamy wartość atrybutu class

	document.write(div.className); // "   a a c   "
	document.write("<br>");
	document.write(showTokens(div.classList)); // "a c"

	document.write("<br><br>");

	div.classList.add("d"); // dodajemy klasę d

	document.write(div.className); // "a c d" - automatyczna aktualizacja (z wyczyszczeniem zbędnych rzeczy)
	document.write("<br>");
	document.write(showTokens(div.classList)); // "a c d"

	document.write("<br><br>");

	var div2 = document.createElement("div"); // referencja do kolejnego kontenera DIV

	document.write(div2.hasAttribute("class")); // false
	document.write("<br>");
	document.write(div2.getAttribute("class")); // null
	document.write("<br>");
	document.write(div2.className.length); // 0
	document.write("<br>");
	document.write(div2.classList.length); // 0

	div2.classList.add();
	document.write("<br><br>");

	document.write(div2.hasAttribute("class")); // true, w Chrome false - błąd
	document.write("<br>");
	document.write(div2.getAttribute("class")); // "", w Chrome null - błąd
	document.write("<br>");
	document.write(div2.className.length); // 0
	document.write("<br>");
	document.write(div2.classList.length); // 0

</script>

Trzeba sobie zdawać sprawę z takich drobnych niuansów. Zestawy są nowością w DOM4 i nie powinny wpływać na działanie już utworzonego kodu - automatyczna aktualizacja klas elementów (usunięcie duplikatów i zwielokrotnionych spacji) odbywa się dopiero po interakcji z obiektem zestawu, i z perspektywy starszych przeglądarek nigdy nie wystąpi. Na chwilę obecną żadna nowoczesna przeglądarka internetowa nie przestrzega unikatowości elementów w zestawach, ale z biegiem czasu powinno się to zmienić (Mozilla - Bug 869788).

Pozostało jeszcze wyjaśnienie uporządkowania w zestawach. Termin ten oznacza, że elementy w zestawie znajdują się w stałym porządku. Dodawanie nowych elementów nie powinno wpływać na już istniejący porządek, przez co kolejność zastosowana przez programistę zostaje zachowana.

Uporządkowania nie należy mylić z sortowaniem, które zmienia porządek elementów w zestawie.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<script>

	function showTokens(set){
		return Array.prototype.slice.call(set).join(" ");
	}

	var div = document.createElement("div"); // referencja do kontenera DIV

	div.className = "a a c"; // ustawiamy wartość atrybutu class

	document.write(div.className); // "a a c"
	document.write("<br>");
	document.write(showTokens(div.classList)); // "a c"

	document.write("<br><br>");

	div.classList.add("b"); // dodajemy klasę b

	document.write(div.className); // "a c b" - automatyczna aktualizacja (z wyczyszczeniem zbędnych rzeczy)
	document.write("<br>");
	document.write(showTokens(div.classList)); // "a c b"

</script>

Automatyczne przekierowanie na inną właściwość przy ustawianiu#

Wszystkie właściwości pojawiające się w różnych specyfikacjach, które zwracają obiekt typu DOMTokenList, są definiowane na zasadzie automatycznego przekierowania przy zapisie na właściwość DOMTokenList.value. Odbywa się to poprzez zastosowanie w definicji Web IDL specjalnego atrybutu rozszerzającego [PutForwards].

Przykładowo specyfikacja HTML5 dla właściwość HTMLLinkElement.sizes ma następującą definicję:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
[SameObject, PutForwards=value] readonly attribute DOMTokenList sizes;

Oznacza to, że w trakcie ustawiania nowej wartości poprzez właściwość HTMLLinkElement.sizes tak naprawdę nastąpi samoczynne przekierowanie na właściwość DOMTokenList.value i wykonanie odpowiednich czynności ustawiających. Technika ta ma na celu umożliwienie jeszcze łatwiejszego modyfikowania zestawu słów DOM i powiązanego z nim atrybutu, bo dzięki niej odpada konieczność bezpośredniego sięgania po właściwość DOMTokenList.value.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<!DOCTYPE html>
<html>

<head>

	<link sizes="32x32">

	<script>

		window.onload = function(){

			function showTokens(set){
				return Array.prototype.slice.call(set).join(" ");
			}

			var box = document.getElementById("box"); // referencja do kontenera
			var link = document.getElementsByTagName("link")[0]; // referencja do elementu link

			var sizes_attr = link.attributes[0];

			box.innerHTML += "link.sizes.value: " + link.sizes.value // "32x32" - odczyt atrybutu przez zestaw
				+ "<br> sizes_attr.value: " + sizes_attr.value // "32x32" - jawny odczyt atrybutu
				+ "<br> showTokens(link.sizes): " + showTokens(link.sizes); // "32x32" - odczyt zestawu

			link.sizes = "32x32 48x48"; // zmiana atrybutu i zestawu na zasadzie automatycznego przekierowania

			box.innerHTML += "<br><br>link.sizes.value: " + link.sizes.value // "32x32 48x48" - odczyt atrybutu przez zestaw
				+ "<br> sizes_attr.value: " + sizes_attr.value // "32x32 48x48" - jawny odczyt atrybutu
				+ "<br> showTokens(link.sizes): " + showTokens(link.sizes); // "32x32 48x48" - odczyt zestawu

			link.sizes.value = "32x32 48x48 48x48"; // jawna zmiana atrybutu i zestawu

			box.innerHTML += "<br><br>link.sizes.value: " + link.sizes.value // "32x32 48x48 48x48" - odczyt atrybutu przez zestaw
				+ "<br> sizes_attr.value: " + sizes_attr.value // "32x32 48x48 48x48" - jawny odczyt atrybutu
				+ "<br> showTokens(link.sizes): " + showTokens(link.sizes); // "32x32 48x48" - odczyt zestawu

			box.innerHTML += "<br><br>typeof link.sizes: " + typeof link.sizes; // object
			box.innerHTML += "<br>link.sizes.constructor: " + link.sizes.constructor; // function DOMTokenList() { [native code] }
			box.innerHTML += "<br>link.sizes.add: " + link.sizes.add; // function add() { [native code] }

		}

	</script>

</head>

<body>
	<div id="box">
</body>

</html>

Zmienna liczba argumentów w metodach#

Metody zestawów takie jak DOMTokenList.add() i DOMTokenList.remove() mogą przyjmować zmienną liczbę argumentów. Nic w tym nadzwyczajnego, ale w ich przypadku można całkowicie pominąć argument i nie będzie to błędem DOM. W takiej sytuacji niczego nie dodajemy/usuwamy z uporządkowanego zestawu słów. Z drugiej jednak strony wywołanie tych metod uaktualni wartość atrybutu skojarzonego z zestawem. Może być to przydatne w sytuacji, kiedy chcemy pozbyć się duplikatów i zbędnych znaków spacji umieszczonych w skojarzonym atrybucie.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<script>

	function showTokens(set){
		return Array.prototype.slice.call(set).join(" ");
	}

	var div = document.createElement("div"); // referencja do kontenera DIV

	div.className = "   a a b b c c   "; // ustawiamy wartość atrybutu class

	document.write(div.className); // "   a a b b c c   "
	document.write("<br>");
	document.write(showTokens(div.classList)); // "a b c"

	document.write("<br><br>");

	div.classList.add(); // bez przekazania argumentu, może być też metoda remove()

	document.write(div.className); // "a b c" - automatyczna aktualizacja
	document.write("<br>");
	document.write(showTokens(div.classList)); // "a b c" - bez zmian

</script>

Wykrywanie za pomocą zestawów obsługi niektórych opcji#

Specyfikacja HTML5 dla niektórych atrybutów skojarzonych z zestawami definiuje wartości o specjalnym znaczeniu, jak chociażby dla atrybutu sandbox w elemencie iframe, atrybutu rel w elementach link, a i area lub atrybutu dropzone we wszystkich elementach HTML. Niektóre z tych wartości mają wpływ na "proces przetwarzania przez przeglądarkę internetową" i dobrze byłoby udostępnić jakiś mechanizm, który pozwoliłby ustalić, czy opcja reprezentowana przez daną wartość jest już obsługiwana przez daną przeglądarkę internetową.

Po dziwnych próbach zaadaptowania do tego celu już istniejących metod DOMTokenList.add(), DOMTokenList.replace() i DOMTokenList.toggle() ostatecznie zdecydowano, że najwłaściwszym będzie zdefiniowane nowej metody DOMTokenList.supports(), która wykryje zaimplementowanie przez daną przeglądarkę określonej funkcjonalności na podstawie obecności przekazanego do niej argumentu w tzw. obsługiwanych słowach.

Trzeba wyraźnie podkreślić, że nie wszystkie wartości o specjalnym znaczeniu wymienione dla jakiegoś atrybutu znajdują się w obsługiwanych słowach, trafiają tam jedynie te wartości, które wpływają na proces przetwarzania i w danym momencie zostały już zaimplementowane przez daną przeglądarkę. Spośród wielu wymienionych wartości dla atrybutu rel w elementach a i area tylko noreferrer, noopener oraz prefetch mogą znajdować się w obsługiwanych słowach, a w przypadku elementów link mamy do czynienia z wartościami alternate, icon, pingback, prefetch, stylesheet oraz next.

Prosty przykład:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<script>

	var element_dropzone_tokens = [
		"copy", "move", "link", "string:test1", "string:test2", "file:test1", "file:test2"
	];
	var iframe_sandbox_tokens = [
		"allow-forms", "allow-modals", "allow-pointer-lock", "allow-popups",
		"allow-popups-to-escape-sandbox", "allow-same-origin", "allow-scripts", "allow-top-navigation"
	];
	var a_area_rel_tokens = [
		"noreferrer", "noopener", "prefetch"
	];
	var link_rel_tokens = [
		"alternate", "icon", "pingback", "prefetch", "stylesheet", "next"
	];
	var other_rel_values = [
		"author", "bookmark", "external", "help", "license", "nofollow", "prev", "search", "tag"
	];
	var fiction_values = ["", " ", "test"];

	var el_a = document.createElement("a");
	var el_area = document.createElement("area");
	var el_link = document.createElement("link");
	var el_iframe = document.createElement("iframe");
	var el_html = document.documentElement;

	function getSupportsInfo(desc, el, attr, tokens, values){

		document.write(desc + "<br>");

		var result_start = el.localName + "." + attr + ".supports('";
		var tokens = values ? tokens.concat(values, fiction_values) : tokens.concat(fiction_values);
		var tokensLen = tokens.length;

		for (var i = 0; i < tokensLen; i++){

			var token = tokens[i];
			var token_up = token.toUpperCase();

			document.write(result_start + token + "'): " + el[attr].supports(token) + "<br>");
			document.write(result_start + token_up + "'): " + el[attr].supports(token_up) + "<br>");

		}

		document.write("<br>");

	}

	var desc_start = "Testy obsługiwanych słów dla elementu ";

	getSupportsInfo(desc_start + "IFRAME i atrybutu SANDBOX:", el_iframe, "sandbox", iframe_sandbox_tokens);
	getSupportsInfo(desc_start + "LINK i atrybutu REL:", el_link, "relList", link_rel_tokens, other_rel_values);
	getSupportsInfo(desc_start + "A i atrybutu REL:", el_a, "relList", a_area_rel_tokens, other_rel_values);
	getSupportsInfo(desc_start + "AREA i atrybutu REL:", el_area, "relList", a_area_rel_tokens, other_rel_values);
	getSupportsInfo(desc_start + "HTML i atrybutu DROPZONE:", el_html, "dropzone", element_dropzone_tokens);

</script>
Pasek społecznościowy

SPIS TREŚCI AKTUALNEJ STRONY

Podstawy (H1) Zestawy (H2) Interfejsy i polecenia (H3) Aktualność zestawów (H3) Unikatowość i uporządkowanie elementów w zestawach (H3) Automatyczne przekierowanie na inną właściwość przy ustawianiu (H3) Zmienna liczba argumentów w metodach (H3) Wykrywanie za pomocą zestawów obsługi niektórych opcji (H3)