JavaScript/Anwendung und Praxis/Kontextmenü

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Das Kontextmenü einer Seite ist manipulierbar (es kann unterbunden und simuliert werden). Das wollen wir einmal ausprobieren: (Rechtsklick auf „ansehen“ → „in neuem Tab öffnen“ o.ä.)

Beispiel ansehen …
  1.     var cursorx, cursory, cmenu;
  2.     cmenu = true;
  3.  
  4.     cursorx = cursory = 0;
  5.  
  6.     function create_id(element) {
  7.       if (typeof element != "object" || element == null) return false;
  8.       if (typeof element.id != "string" || !element.id) {
  9.         var no = 0;
  10.         while(document.getElementById("el_"+no)) no++;
  11.           element.id = "el_"+no;
  12.         }
  13.         return element.id;
  14.       }
  15.  
  16.     function handle_cursor(e) {
  17.       if (!e) e = event;
  18.         try {
  19.           cursorx = e.clientX;
  20.           cursory = e.clientY;
  21.         } catch (e) {
  22.  
  23.         try {
  24.           cursorx = e.screenX;
  25.           cursory = e.screenY;
  26.         } catch (e) {}
  27.  
  28.       }
  29.       return true;
  30.     }
  31.  
  32.     document.onmousemove = onmousemove = onmousedown = document.onmousedown = handle_cursor;
  33.  
  34.     onload = function () {document.body.onmousemove = document.body.onmousedown = handle_cursor;};
  35.  
  36.     function contextmenu() {
  37.       if (!cmenu || cmenu === null) return true;
  38.  
  39.       hide("show");
  40.         var element = get_focused_element();
  41.         if (typeof element != "object" || !element) {
  42.           hide_context();
  43.           return true;
  44.         }
  45.         create_id(element);
  46.         var output = '';
  47.         output = '<tr><th>Element: '+element.id+' ('+element.tagName.toLowerCase()+')<\/th><\/tr>'; // Nur zum Test
  48.         output += '<tr><td onclick="hide(\''+element.id+'\')">Verstecken<\/td><\/tr>';
  49.  
  50.         switch(element.tagName.toLowerCase()) {
  51.           case "img":
  52.             output += '<tr><td onclick="show_img(\''+element.id+'\')">Anzeigen<\/td><\/tr>';
  53.             output += '<tr><td onclick="show_attr(\''+element.id+'\', \'src\')">Quelle anzeigen<\/td><\/tr>';
  54.           break;
  55.  
  56.           case "a":
  57.           case "iframe":
  58.             output += '<tr><td onclick="show_attr(\''+element.id+'\', \'href\')">Ziel anzeigen<\/td><\/tr>';
  59.           break;
  60.         }
  61.  
  62.         output += '<tr><td onclick="cmenu = false;">Immer normales Kontextmenü<\/td><\/tr>';
  63.         output += '<tr><td onclick="setTimeout(function () {cmenu = null;}, 1);">Einmal normales Kontextmenü<\/td><\/tr>';
  64.         output += '<tr><td onclick="transparent(\''+element.id+'\', \'-\');">Transparenter<\/td><\/tr>';
  65.         if (get_opacity(element) < 1) {
  66.           output += '<tr><td onclick="transparent(\''+element.id+'\', \'+\');">Weniger Transparent<\/td><\/tr>';
  67.         }
  68.         var contexttable = document.getElementById("contexttable");
  69.         contexttable.innerHTML = output;
  70.         contexttable.style.display = "block";
  71.         var cl = contexttable.getBoundingClientRect();
  72.  
  73.         if (cursory + cl.height < innerHeight) {
  74.           contexttable.style.top = cursory+"px";
  75.         } else {
  76.           contexttable.style.top = (cursory-cl.height)+"px";
  77.         }
  78.         if (cursorx + cl.width < innerWidth) {
  79.           contexttable.style.left = cursorx+"px";
  80.         } else {
  81.           contexttable.style.left = (cursorx-cl.width)+"px";
  82.         }
  83.         return false;
  84.    }
  85.  
  86.     function show(text) {
  87.         var div = document.getElementById("show");
  88.         div.style.display = "block";
  89.         div.innerHTML = text + '<br><div align="center"><input type="button" onclick="hide(\'show\');" value="OK"><\/div>';
  90.         var cl = div.getBoundingClientRect();
  91.         var x = (innerWidth - cl.width) / 2;
  92.         var y = (innerHeight - cl.height) / 2;
  93.         div.style.top = y+"px";
  94.         div.style.left = x+"px";
  95.     }
  96.  
  97.     function get_focused_element() {
  98.         var elements = document.getElementsByTagName("*");
  99.         var focused_element = null;
  100.         var body_came = false;
  101.         var cl;
  102.         for (var i = 0;i < elements.length;i++) {
  103.           if (elements[i].tagName.toLowerCase() == "body") {
  104.             body_came = true;
  105.             continue;
  106.          }
  107.           if (!body_came) continue;
  108.             cl = elements[i].getBoundingClientRect();
  109.             var height, width, x, y;
  110.             height = cl.height;
  111.             width = cl.width;
  112.             x = cl.left;
  113.             y = cl.top;
  114. 			if (cursorx > x && cursory > y && cursorx < x + width && cursory < y + height) {
  115.               focused_element = elements[i];
  116.             }
  117.           }
  118.           return focused_element;
  119.     }
  120.  
  121.     function show_img(img) {
  122.           img = document.getElementById(img);
  123.           show('<a href="'+img.src+'"><img src="'+img.src+'"><\/a>');
  124.     }
  125.  
  126.     function hide(element) {
  127.           if (typeof element == "string") element = document.getElementById(element);
  128.           if (typeof element != "object") return false;
  129.              element.style.display = "none";
  130.     }
  131.  
  132.     function show_attr(element, attr) {
  133.           if (typeof element == "string") element = document.getElementById(element);
  134.           show(element[attr]);
  135.     }
  136.  
  137.     function hide_context() {
  138.           hide("contexttable");
  139.     }
  140.  
  141.     onclick = function (e) {
  142.          if (e.which == 3) {
  143.            return contextmenu();
  144.          }
  145.          hide_context();
  146.          if (cmenu === null) cmenu = true;
  147.     }
  148.  
  149.     function transparent(obj, dir) {
  150.          if (typeof obj == "string") obj = document.getElementById(obj);
  151.          if (typeof obj != "object") {
  152.            throw "transparent: first arg not found!";
  153.            return false;
  154.          }
  155.          var op = get_opacity(obj);
  156.          eval("op "+dir+"= 0.5");
  157.          if (op > 1) op = 1;
  158.          if (op < 0) op = 0;
  159.          obj.style.opacity = op;
  160.     }
  161.  
  162.     function get_opacity(obj) {
  163.           if (!obj.style.opacity) return 1;
  164.            return Number(obj.style.opacity);
  165.     }

[Bearbeiten] Erläuterung

Die zentrale Funktion dieser Seite ist die Funktion contextmenu(), die ein Kontextmenü simuliert. Deren Rückgabewert wird von der body.oncontextmenu-Funktion zurückgegeben, sodass, sofern dieser false ist, die Anzeige des "normalen" Kontextmenüs unterbunden wird. Die Verwendung der Variable cmenu wird unten erklärt. Außerdem wird ein ggf. vorhandenes internes Dialogfenster geschlossen hide("show");. Danach wird mit get_focused_element das Element, auf das der Mauscursor zeigt, auf das demnach geklickt wurde, ermittelt.

Dazu wird mittels document.getElementsByTagName die Liste aller (*) Elemente ermittelt. Diese werden in einer for-in - Schleife abgearbeitet. Hierbei sind aber nur die Elemente nach dem body relevant, sodass, falls die Schleife noch nicht am body war (body_came == false), der Schleifendurchlauf mittels continue übersprungen wird. Sollte die Schleife gerade am body sein, so wird markiert, dass der body bereits kam (body_came = true) und auch dieser Schleifendurchlauf übersprungen (continue).


Erreichen wir daher Zeile 102, so ist klar, dass es sich mit elements[i] um ein Element nach dem body handelt, also um eines, das angezeigt werden kann. Dessen tatsächliche Ausmaße bestimmt dann die Funktion getBoundingClientRect(). Diese Werte werden zwecks einfacherer Verarbeitung in den Variablen x, y, left und right gespeichert (top ist reserviert). Nur wenn

  1. Der Cursor rechts vom rechten Rand des Objektes ist (cursorx > x)
  2. Der Cursor unterhalb vom oberen Rand ist (cursory > y)
  3. Der Cursor links vom linken Rand (rechter Rand + Breite) ist (cursorx < x + width)
  4. Der Cursor oberhalb vom unteren Rand (oberer Rand + Höhe) ist (cursory < y + height)

befindet sich der Cursor auf dem Objekt. Woher aber wissen wir, wo der Cursor ist? Ganz einfach: Bei jeder Mausbewegung auf dem Fenster, dem document oder dem body, sowie bei jedem Klick wird die Position von der Funktion handle_cursor() in den Variablen cursorx und -y gespeichert. Zwecks eines Kontextmenüs ist es aber wichtig, möglichst genau das Element zu kennen (wird auf ein Bild innerhalb eines div-Bereiches innerhalb eines Textabsatzes geklickt, wollen wir das Bild und nichts anderes haben). Deshalb wird es in der Variable focused_element gespeichert und die Schleife nicht abgebrochen. Das innerste solcher Elemente ist zwangsläufig auch das letzte. Dieses wird mittels return an contextmenu zurückgegeben. Sollte dies kein object sein, so wird ein ggf. geöffnetes Kontextmenü geschlossen und die Funktion mit true beendet, was zur Anzeige des "normalen" Kontextmenüs führt. Dies kann passieren, wenn an der Stelle des Klickens kein Objekt ist (z.B. unterhalb einer kurzen Seite). Solle dieses aber ein Objekt sein, so fährt die Funktion in Zeile 45 fort, indem sie ein id-Attribut für das Element erzwingt.

Die hierzu verwendete Funktion create_id() möchte ich an dieser Stelle kurz erklären. Sollte das Element kein id-Attribut haben, so wird eines in der Form el_n erzeugt, wobei n eine durchlaufende Nummer ist. Hierbei wird in einer while-Schleife die gerade aktuelle Nummer ermittelt, indem die Existenz eines Objektes mit einer el_n id, wobei n immer no ist und damit ständig erhöht wird, überprüft wird. Sobald diese Abfrage fehlschlägt, ist die id noch frei und wird element als id gesetzt. Die Funktion gibt die eben neu erzeugte oder die alte id zurück, dieses Ergebnis wird von contextmenu aber nicht verarbeitet. Stattdessen wird hier die Variable output intalisiert, welche den HTML-Inhalt des Kontextmenüs (eine Tabelle) enthält. Zu Test wird gleich noch eine Zeile angehängt, welche die id und den Tagnamen des Elements enthält. Dies ist zur Fehlersuche z.T. ungemein praktisch, aber für den Onlineeinsatz irreführend. Außerdem wird eine Zeile angefügt, die es ermöglicht, das Element zu verstecken. Über diese, wie über andere Optionen, sei beim Onlineeinsatz heftig nachzudenken, der Ersatz des Kontextmenüs an sich ist stark fragwürdig, aber lustig und als Beispiel geeignet. Anschließend wird im Zuge einer switch-Fallunterscheidung der Elementname in Kleinbuchstaben (toLowerCase()) mit verschiedenen Fällen überprüft.

Daraus ergeben sich verschiedene Szenarien:

  1. "img": Es handelt sich um ein Bild. Daher werden Optionen zur Anzeige des Bildes bzw. dessen Url gegeben.
  2. "a" bzw. "iframe": Es handelt sich in gewisser Hinsicht immer um eine andere Website (bei 'a' nur als Link), weshalb die Option geboten wird, deren Url anzuzeigen.

Es können so alle möglichen weiteren Szenarien durchgespielt werden (Formulare absenden, zurücksetzen etc.), für dieses Beispiel reichen aber diese drei. Außerdem wird die Möglichkeit gegeben, die Anzeige des "normalen" Kontextmenüs einfach oder dauerhaft zu ermöglichen. Dies ist unbedingt nötig, will man den Besucher nicht vollends verärgern. Des Weiteren gibt es die eher sinnlose Möglichkeit, die Transparenz des Elementes zu erhöhen bzw., falls dieses nicht vollständig undurchsichtig ist (opacity < 1) selbige zu erniedrigen. Anschließend wird in der Variable contexttable eine Referenz auf die Kontext-Tabelle gespeichert. Über diese können wir dann unseren output als HTML unter der Eigenschaft innerHTML speichern. Daraufhin setzen wir die display-Eigenschaft auf block, was zur Anzeige des Elementes führt, und ermitteln dessen Höhe und Breite mittels getBoundingClientRect(). Das müssen wir wissen, um das Kontextmenü sichtbar zu positionieren, denn, ist der Cursor zu weit unten, würde es abgeschnitten. Normalerweise wird dann das Kontextmenü nach oben hin geöffnet, wieso dann nicht auch hier. Wir überprüfen daher, ob die Höhe des Kontextmenüs (cl.height), addiert mit der Cursor-Y-Position (cursory), größer als die Fensterhöhe ist (innerHeight). Sollte dies true ergeben, so wird das Kontextmenü nach oben hin geöffnet (y-Wert = Cursor-Y-Wert - Höhe), sonst nach unten (y-Wert = Cursor-Y-Wert). Der Y-Wert ist die CSS-top Eigenschaft. Analog öffnen wir das Menü entweder nach links oder rechts (Stichworte: x, width, left, innerWidth). Zu guter Letzt gibt die Funktion false zurück, sodass das "normale" Kontextmenü nicht angezeigt wird.

Somit wird das Kontextmenü bei Rechtsklick geöffnet. Wir brauchen nun nur noch 1. die Aktionen auszuführen und 2. das Kontextmenü wieder zu schließen. Wenden wir uns zuerst letzterem zu: Um das Kontextmenü zu schließen gibt es die Funktion hide_context(). Diese blendet mithilfe der hide-Funktion (s.u.) das Kontextmenü aus. Diese wird auf Klick (onclick) ausgeführt, sofern nicht die rechte Maustaste (e.which == 3) gedrückt wurde. In diesem Fall nämlich wird das Kontextmenü geöffnet. Diese Option ist wichtig, falls ein Browser so standardkonform ist, dass er das oncontextmenu-Event nicht unterstützt. Die Verarbeitung der Variable cmenu wird weiter unten erklärt. Wir wenden uns jetzt den verschiedenen Aktionen des Kontextmenüs zu:

[Bearbeiten] Aktionen des Kontextmenüs

1. Verstecken: Das ruft die hide-Funktion auf, die das Element verstecken soll: Hierzu wird zunächst, sollte es sich bei element um einen String handeln, dieser als Element-id behandelt und das tatsächliche Objekt mittels document.getElementById erzeugt. Anschließend wird dessen display-style-Eigenschaft auf "none" gesetzt, was dessen Anzeige unterdrückt.

2. Anzeigen: Das ruft die Funktion show_img auf: Zunächst wird wie bei hide das Objekt ggf. aus der Element-Id erzeugt. Anschließend wird ein HTML-Fragment, bestehend aus dem Bild und einem Link dorthin, an die Funktion show übergeben. Diese ist der alert-Funktion durchaus ähnlich, allerdings ist sie HTML-basiert, sodass HTML-Tags verarbeitet werden können. Hierzu wird das Fragment (Parameter text) als innerHTML nebst einem Button zum Schließen desselben an den div-Bereich #show übergeben und dessen Anzeige mittels div.style.display = "block" ermöglicht. Die Ermittlung von Höhe und Breite mittels getBoundingClientRect() wird oben beschrieben. Wir setzen den Bereich zentriert. Dazu ist dessen X-Wert (style.left) exakt die halbe Bildschirmbreite minus die halbe Breite des Bereiches selbst. Wir schreiben dies nur verkürzt: innerWidth / 2 - cl.width / 2 = (innerWidth - cl.width) / 2 (Distributivgesetz). Analog verfahren wir mit dem Y-Wert.

3. Quelle anzeigen: Ruft die Funktion show_attr auf mit 2. Parameter "src" auf: Diese übergibt - ggf. nach dem Erzeugen eines Objektes via document.getElementById - die [2. Parameter, hier "src"]-Eigenschaft an die Funktion show (s.o.).

4. Ziel anzeigen: Wie Quelle anzeigen, nur, dass mit href statt src.

5. Immer normales Kontextmenü: Hierzu wird die Variable cmenu auf false gesetzt. Diese muss jetzt aber unbedingt erklärt werden:

Wert erzeugt durch Bedeutung
true default / onclick, wenn null eigenes Kontextmenü wird aufgerufen
null Schaltfläche "Einmal normales Kontextmenü" Das "normale" Kontextmenü wird beim nächsten Mal aufgerufen, cmenu dann aber wieder auf true gesetzt.
false Schaltfläche "Immer normales Kontextmenü" Das "normale" Kontextmenü wird aufgerufen

Um von null auf true zu kommen, wird die onclick-Funktion verwendet. Sollte cmenu nicht true sein, so wird die contextmenu-Funktion sofort mit true beendet.

6. Transparenter bzw. Weniger Transparenz Hierzu wird das Objekt auf die nun schon bekannt Weise ggf. erzeugt oder abgebrochen. Daraufhin wird der CSS-opacity Wert mittels get_opacity ermittelt. Diese Funktion prüft zunächst, ob ein solcher vorhanden ist. Wenn ja, so wird dieser als Zahl (Number) zurückgegeben. Andernfalls ist das Element nicht transparent, und es wird der opacity-Wert für nicht transparent - nämlich 1 - zurückgegeben. Dieser wird von der transparent-Funktion entweder um 0.5 erhöht oder um dieselbe Zahl erniedrigt. Eine if-Überprüfung, ob der zweite Parameter dir (engl. direction, also Richtung) "+" oder "-" ist, kann mittels eval vermieden werden. Wir setzen dessen Wert einfach in den Ausdruck ein, sodass entweder op += 0.5 oder op -= 0.5 entsteht. Anschließend setzen die beiden if-Anweisungen den Wert zwischen 0 und 1. Dieser wird daraufhin als neuer opacity-Wert verwendet.

[Bearbeiten] Die Besonderheiten in CSS

Diese Seite hat zwei interessante CSS-Eigenschaften:

  1. Alle Listenpunkte werden durchnummeriert
  2. Die Schaltflächen im Kontextmenü werden beim Mausüberfahren gefärbt

Beides ließe sich natürlich kompliziert mit JavaScript durchsetzen, aber mit CSS geht das schneller. Zu den Listenpunkten: Wir haben einen CSS-counter, hier cnt. Dieser wird beim body auf 0 gesetzt, bei jedem Listenpunkt erhöht und hinter jeden nebst Leerzeichen angehängt (siehe style-Teil). Für das Kontextmenü genügt der etwas eigenwillige Selektor table.system [onclick]:hover, was auf gut deutsch heißt: "Alles, innerhalb einer Tabelle, deren Klasse system heißt, was ein onclick-Attribut besitzt und von der Maus überfahren wird". Die Kontextmenütabelle ist von der Klase system (selbstdefiniert), die Schaltflächen besitzen alle onclick-Attribute.

Meine Werkzeuge
Namensräume

Varianten
Aktionen
Übersicht
Index
Mitmachen
Werkzeuge
Spenden
SELFHTML