[JS] OnMouseOut czy SetTimeOut - zła synchronizacja w menu

0

Witam,
mam problem z działaniem onmouseout i onmouseover w rozwijanym menu. Kiedy najadę na rozwiniętą cześć menu, znika uzupełnienie półkola i pryska cały efekt. W javascripcie musiałem dodać setTimeout, aby poprawić efekt rozwijanego menu, bez tego ten problem nie występował… Pomóżcie?!

http://www.premiertours.pl/strona/

<div class="menu1_out" id="wybor1">
  <ul class="topnav" onmouseover="changeclass(1);" onmouseout="changeclass(0);">
    <li>
      <span>BILETY</span>
      <ul class="subnav">
        <li><a href="#">LOTNICZE</a></li>
        <li><a href="#">AUTOKAROWE</a></li>
        <li><a href="#">PROMOWE</a></li>
        <li><a href="#">IMPREZY</a></li>
      </ul>
    </li>
  </ul>
</div>
<div class="menu2_out" id="wybor2">
  <ul class="topnav" onmouseover="changeclass(3);" onmouseout="changeclass(2);">
    <li>
      <span>WYCIECZKI</span>
      <ul class="subnav">
        <li><a href="#">LAST MINUTE</a></li>
        <li><a href="#">FIRST MINUTE</a></li>
        <li><a href="#">DOJAZD WŁASNY</a></li>
        <li><a href="#">DLA RODZIN</a></li>
      </ul>
    </li>
  </ul>
</div>

oraz javascript

<!--
function changeclass(gdzie) {
  if (gdzie == 0)
  {
    setTimeout("document.getElementById('wybor1').className= 'menu1_out';", 570)
  }
  if (gdzie == 1)
  {
    document.getElementById('wybor1').className= 'menu1_in';
  }
  if (gdzie == 2)
  {
    setTimeout("document.getElementById('wybor2').className= 'menu2_out';", 570)
  }
  if (gdzie == 3)
  {
    document.getElementById('wybor2').className= 'menu2_in';
  }
}
//-->
0

Ja bym z-indexami wywindował podmenusy wyżej, wstawił zaślepkę w postaci pustego li, dodał padding, może ujemny margines dla listy, albo nawet bez z-indeksu.... może wtedy nie będzie tej białości...

Zresztą może najłatwiej podnieś podlistę marginesem i dodaj górny padding dla niej...

Poza tym może przemyśl taki wygląd nawigacji.

0

Niestety, to wszystko nie pomaga... To menu jest bardzo skomplikowane, ze względu na półprzeźroczystość elementy nie mogą na siebie zachodzić - wstawię iframe z przykładowym tekstem, aby było widać co jest...

0

@Jet:
A powiedz no kiedy ten Twój dorobiony skrypcik ma dać klasę menuX_in a kiedy menuX_out? Przykładowo, gdy najechałem na opcję Bilety (w menu1) i jeżdżę sobie kursorem po podopcjach Lotnicze, Autokarowe i Promowe, to cały czas ma być menu1_in? Czy ma się to jakoś zmieniać? Lepiej chyba będzie gdy precyzyjnie wytłumaczysz co to ma robić, bo obawiam się, że Twój kod jest... nieoptymalny :).

Czy jest tak, że gdy najeżdża się na Bilety i wysuwa się podmenu, to po ok. 570 milisekundach ma się pojawić klasa menu1_in, a menu1_out ma zniknąć i ten stan ma się utrzymywać aż do całkowitego zjechania kursorem z podmenu Bilety -- wtedy po kolejnych 570 ms menu1_in ma zniknąć, a menu1_out ma się pojawić? Potwierdź lub napisz jak ma być.

BTW. Jet nie zdarzasz się być z Poznania? Albo inaczej... spójrz na mój "nick", bo może się znamy, sądząc po Twoim nicku i avatarze.

0

Jak widać rozwijane menu jest z góry płaskie a nie pół okrągłe, więc po najechaniu na napis BILETY od razu ładuje się klasa menuX_in z podmienioną grafiką (płaską) i ma ona trwać cały czas póki menu jest rozwinięte + 570 milisekund, czyli czas zwijania się menu. Wtedy dopiero wraca do menuX_out(półokrągła).

OT: Niestety musiałeś mnie z kimś pomylić, mieszkam w Gdańsku…

0

@Jet:
A myślałem że spotkałem kolegę ze studiów :).

Co do rozwiązania Twojego problemu to masz na stronie jQuery, skorzystaj więc z jego dobrodziejstw. Przede wszystkim trzeba wykorzystać nie mouseover i mouseout, tylko bardzo przydatne, sztuczne zdarzenia mouseenter i mouseleave (warto zauważyć, że wymyślił je... Microsoft :) -- natywnie obsługuje je tylko IE, ale jQuery potrafi je symulować).

Napisałem Ci kod, który powinien być "samowyjaśniający". Dlatego jest taki rozwklekły -- dałoby się go skompresować do kilku linijek.

Po pierwsze, z HTML-a wywalamy niepotrzebne id i brzydkie funkcje obsługujące zdarzenia. One powinny znaleźć się w JavaScripcie, a nie w HTML-u! Dodajemy też klasę "hoverfix" dla identyfikacji elementów, które będą miały otrzymać klasy menuX_in oraz menuX_out:

<div class="hoverfix">
  <ul class="topnav">
    <li>
      <span>BILETY</span>
      <ul class="subnav">
        <li><a href="#">LOTNICZE</a></li>
        <li><a href="#">AUTOKAROWE</a></li>
        <li><a href="#">PROMOWE</a></li>
        <li><a href="#">IMPREZY</a></li>
      </ul>
    </li>
  </ul>
</div>
<div class="hoverfix">
  <ul class="topnav">
    <li>
      <span>WYCIECZKI</span>
      <ul class="subnav">
        <li><a href="#">LAST MINUTE</a></li>
        <li><a href="#">FIRST MINUTE</a></li>
        <li><a href="#">DOJAZD WŁASNY</a></li>
        <li><a href="#">DLA RODZIN</a></li>
      </ul>
    </li>
  </ul>
</div>

A teraz JavaScript. Nie będzie tu żadnego ukrytego evala (eval is evil!), tylko eleganckie programowanie funkcyjne. Ten kod ma się znaleźć w osobnym pliku JS. Plik ten powinien być importowany gdzieś na dole strony; od biedy może być i u góry, ale wtedy całość kodu trzeba wstawić w zdarzeniu ready. Takie małe chamstwo z mojej strony: jeśli mnie nie posłuchasz i wstawisz to w środek strony, to nie wolno Ci tego kodu używać. Taka dziwna licencja. Kod ten może być używany do woli przez każdego, ale tylko wtedy, gdy wstawi się go porządnie :). Nie wolno go używać, gdy jest dodany nieprawidłowo, tj. gdy np. opuści się atrybut type w tagu <script> w wersjach HTML-a, w których jest to niedopuszczalne. Jeśli z jakichś przyczyn naprawdę nie możesz umieścić tego kodu w osobnym pliku (co się zdarza, choć bardzo rzadko), to musisz mi to tutaj najpierw wytłumaczyć i jeśli faktycznie masz taką wyjątkową sytuację, to się zgodzę. To "małe chamstwo" ma oczywiście walory edukacyjne i jeśli już dbasz o jakość kodu, to wymagania licencyjne spełnisz naturalnie.

$('.hoverfix').each(function(i, container) {
  var $container   = $(container)
  ,   $trigger     = $container.find('ul.topnav > li > span:first-child')
  ,   index        = i + 1
  ,   overClass    = 'menu' + index + '_in'
  ,   outClass     = 'menu' + index + '_out'
  ,   outTimeoutId = null
  ,   outDelay     = 570
  ;
  $trigger.mouseenter(setOver);
  $container.mouseleave(waitAndSetOut);
  setOut(); 

  function setOver() {
    clearOutTimeout();
    $container.addClass(overClass).removeClass(outClass);
  }

  function setOut() {
    clearOutTimeout();
    $container.removeClass(overClass).addClass(outClass);
  }
  
  function waitAndSetOut() {
    outTimeoutId = setTimeout(setOut, outDelay);
  }

  function isOutTimeoutSet() {
    return outTimeoutId !== null;
  }

  function clearOutTimeout() {
    if (isOutTimeoutSet()) {
      clearTimeout(outTimeoutId);
      outTimeoutId = null;
    }
  }
});

Jeśli masz jakieś pytania, to wal. Mam nadzieję, że dobrze zrozumiałem o co Ci chodzi. Ten kod daje klasę menuX_in elementowi o klasie hoverfix natychmiast po najechaniu myszą na ten element (a właściwie nie na ten element, ale span będący przyciskiem menu) i utrzymuje tę klasę aż do zjechania myszą z podmenu. Wtedy odczekuje 570 ms, zdejmuję klasę -in i daje -out. Na samym początku skrypt automatycznie daje klasę menuX_out każdemu elementowi, więc nie musisz jej pisać w kodzie HTML (ale możesz).

0

Wszystko gra i trąbi - dzięki :-)

0

Witam,
Na wstępie chcę Ci bardzo podziękować za pomoc w rozwikłaniu mojego problemu z tym menu w jquery.
Pozostał jednak jeszcze jeden mały szczegół z którym nadal nie mogę sobie poradzić. Po najechaniu myszką napis bilety i zjechaniu myszką na rozwiniętą część menu, gdy wrócę do góry i zatrzymam kursor pomiędzy bilety, a rozwijaną częścią menu menuX_in się nie chowa.
Aby uwidocznić problem ustawiłem kolory w css’ie - http://www.premiertours.pl/strona/

Wydaje mi się, że zamiast
$trigger = $container.find('ul.topnav > li > span:first-child')
powinna być tylko klasa ul.topnav li span.subhover, ale nie wiem jak to zapisać...
Z góry dziękuje za pomoc.

0

Zapisuje się takie rzeczy prosto. Funkcji find podajesz po prostu string z selektorem, czyli selektor otoczony apostrofami.

Po pobieżnym przeanalizowaniu struktury menu ja proponuję jednak coś takiego...

Zamiast reagować na zjechanie z elementu .hoverfix, tak jak mamy teraz:

  $container.mouseleave(waitAndSetOut);

reagujmy na zjechanie z elementu .subnav. Czyli tak:

  $container.find('> .topnav > li > .subnav').mouseleave(waitAndSetOut);

Zobacz czy to o to chodzi. Jakby ktoś pytał, czemu Jetowi daję praktycznie gotowy kod... dlatego, że ma dość trudne zadanie z tym menu, że wiele rzeczy zdaje się rozumieć i że sam stara się to wszystko zdebugować. Głupie zaznaczenie elementów w CSS oznacza, że autor tematu przynajmniej się nad tym głowi i sam próbuje coś z tym zrobić.

0

Problem jest nie, co bardziej złożony to, co napisałeś wprawdzie rozwiązuje tą sprawę, ale generuje drugi problem – znacznie poważniejszy. Jeżeli ktoś rozwinie menu, ale nie najedzie na rozwiniętą część(subnav) to menuX_in nie wróci do menuX_out.

Idąc podsuniętym mi przez Ciebie tropem stwierdziłem, że funkcja powinna reagować, gdy kursora nie będzie na '> .topnav > li > .subnav' i 'ul.topnav > li > span:first-child'.

Próbowałem to zapisać tak:

$container.find('> .topnav > li > .subnav' && 'ul.topnav > li > span:first-child').mouseleave(waitAndSetOut);

Pewnie Cię nie zaskoczę – niestety, nie działa…

0

@Jet:
To co napisałeś może świadczyć o niezrozumieniu składni JavaScriptu i selektorów CSS :).

Wywołując find jesteś cały czas "w JavaScripcie" i musisz pisać zgodnie z jego zasadami. Jednak funkcja find traktuje string podany jej za parametr jak selektor CSS, oparty na składni CSS. Ale CSS jest dopiero wewnątrz apostrofów -- z punktu widzenia JavaScriptu to po prostu łańcuch znaków.

Takie wyrażenie:

'> .topnav > li > .subnav' && 'ul.topnav > li > span:first-child'

Zwróci Ci zawsze 'ul.topnav > li > span:first-child', czyli prawy operand operatora &&. A to dlatego, że w JavaScripcie operator && działa następująco: jeśli w wyrażeniu a&&b zmienna a ma wartość prawdziwą, to operator zwraca wartość zmiennej b. W przeciwnym wypadku zwraca wartość zmiennej a. Więcej o tym możesz znaleźć w książce "JavaScript -- Mocne strony" Douglasa Crockforda (zwięzła, tania, świetna -- polecam).

W tym miejscu musisz więc dać jeden ciąg znaków i operator && nic Ci tu nie pomoże.

Z pomocą może przyjść składnia CSS. Umożliwia ona podanie kilku selektorów oddzielonych przecinkami. Złożony w ten sposób selektor dopasuje wszystkie elementy pasujące do któregokolwiek z podselektorów (tych fragmentów oddzielonych przecinkami).

Innymi słowy, to co chciałeś tam zrobić należy zapisać tak:

$container.find('> .topnav > li > .subnav, ul.topnav > li > span:first-child').mouseleave(waitAndSetOut);

To jednak też niewiele tu pomoże -- sprawdź sam.

Spróbuj tak (niestety, jak widzisz, nie mam specjalnie czasu żeby to dokładnie testować):

$('.hoverfix').each(function(i, container) {
  var $container   = $(container)
  ,   $trigger     = $container.find('ul.topnav > li > span:first-child')
  ,   index        = i + 1
  ,   overClass    = 'menu' + index + '_in'
  ,   outClass     = 'menu' + index + '_out'
  ,   outTimeoutId = null
  ,   outDelay     = 570
  ;
  $trigger.mouseenter(setOver);
  
  $container.find('> .topnav > li > .subnav').mouseleave(waitAndSetOut);
  $container.mouseenter(setOver).mouseleave(waitAndSetOut);
  setOut(); 

  function setOver() {
    clearOutTimeout();
    $container.addClass(overClass).removeClass(outClass);
  }

  function setOut() {
    clearOutTimeout();
    $container.removeClass(overClass).addClass(outClass);
  }
  
  function waitAndSetOut() {
    outTimeoutId = setTimeout(setOut, outDelay);
  }

  function isOutTimeoutSet() {
    return outTimeoutId !== null;
  }

  function clearOutTimeout() {
    if (isOutTimeoutSet()) {
      clearTimeout(outTimeoutId);
      outTimeoutId = null;
    }
  }
});

1 użytkowników online, w tym zalogowanych: 0, gości: 1