[jQuery] Wybór kategorii jak na allegro
28 grudnia 2007Kategorie:
Tagi:
Podczas pracy nad pewnym projektem potrzebowałem wyboru kategorii tak jak na allegro przy dodawaniu produktu. Szukałem, szukałem, coś tam znalazłem ale mi się nie podobało. Stwierdziłem, że sam to napiszę, wykorzystując do tego bibliotekę jQuery.
Ogólne założenia:
- Na początku jedna kolumna z głównymi kategoriami
- Po kliknięciu na pozycję usunięcie wszystkich kolumn na prawo
- Ajaxowe sprawdzenie czy dana kategoria posiada podkategorie
- Jeśli tak - wygenerować kolejną kolumnę z listą podkategorii
- Jeśli nie - przekazać id kategorii do inputa
- Możliwość zaznaczenia na początku która kategoria jest wybrana i pokazanie ścieżki kategorii nadrzędnych
- Możliwość wyboru kategorii poprzez wpisanie jej id lub kliknięcie w button
Live demo & download
Update
- dodany parametr określający stałą liczbę kolumn (zbędne są chowane)
Całość możemy podzielić na trzy części
- index.html - inicjalizacja
- select.js - plik pluginu
- ajax.php - plik do którego będziemy się odwoływać
No to ruszamy
Początek - index.html
To może od razu przejdę do rzeczy.
Dodajemy w sekcji head:
<script type="text/javascript" src="jquery.js"></script> <script type="text/javascript" src="select.js"></script>
Dodajemy diva - kontener na kolumny, input na id i kilka buttonów (opcjonalnie)
<div id="selectDiv"></div> <input type="text" id="selectValue" /> <input value="1" type="button" id="btn1" /> <input value="2" type="button" id="btn2" /> <input value="14" type="button" id="btn3" />
Teraz konfiguracja pluginu. Możliwe opcje:
- urlSub - url do pobrania podkategori
- urlParent - url do pobrania ścieżki kategorii nadrzędnych
- input - input na id
- size - ilość pozycji w kolumnie (domyślnie: 10)
- start - id kategori zaznaczonej na początku (domyślnie: 0)
- type - typ ajaxowego zapytania (domyślnie: 'get')
- columns - ilość kolumn, 0 - bez limitu (domyślnie: 3)
<script type="text/javascript"> $(document).ready(function(){ $('#selectDiv').catSelect({ urlSub: 'get.php?func=sub', urlParent: 'get.php?func=parent', input: $('#selectValue'), size: 5, start: 14, type: 'get', columns: 3 }); // dodanie event handlerów do buttonów (te funkcje można wywołać praktycznie z dowolnego miejsca w kodzie) $('#btn1').click(function(){$.fn.catSelect.selectById(1)}); $('#btn2').click(function(){$.fn.catSelect.selectById(2)}); $('#btn3').click(function(){$.fn.catSelect.selectById(14)}); }); </script>
select.js - czy jak to wszystko działa
Poniżej jest dość sporo samego kodu. Jeśli kogoś nie interesuje jak działa plugin może przejść od razu do kolejnej sekcji
// deklaracja pluginu $.fn.catSelect = function(options){ var defaults = { urlSub: '', urlParent: '', size: 10, type: 'get', container: $(this), input: null, start: 0 }; $.fn.catSelect.settings = $.extend(defaults, options); // wyczyszczenie kontenera $.fn.catSelect.reset(); // event handler inputa $.fn.catSelect.settings.input.keyup(function(){ $.fn.catSelect.selectById(this.value); }); if($.fn.catSelect.settings.start != 0){ // pierwszy request $.fn.catSelect.selectById($.fn.catSelect.settings.start); } else { $.fn.catSelect.onChange(); } } $.fn.catSelect.cache = []; // cache podkategori $.fn.catSelect.pCache = []; // cache kategori nadrzędnych $.fn.catSelect.obj = null; // ostatnia kolumna
Zacznijmy od $.fn.catSelect.reset();
$.fn.catSelect.reset = function(){ $.fn.catSelect.settings.container.html(''); // wyczyszczenie kontenera $.fn.catSelect.obj = $(document.createElement('select')); // ustawienie domyślnego obiektu }
Teraz $.fn.catSelect.onChange() - wywoływane przy każdej zmianie kategori
$.fn.catSelect.onChange = function(){ var id = $.fn.catSelect.obj.val() || 0; // sprawdzenie czy podkategorie są w cache if($.fn.catSelect.cache[id]){ $.fn.catSelect.onSuccess($.fn.catSelect.cache[id]); } else { // pobranie kategorii $.fn.catSelect.get(id); } }
$.fn.catSelect.get = function(id){ // ajax request $.ajax({ url: $.fn.catSelect.settings.urlSub, type: $.fn.catSelect.settings.type, data: 'id='+id, dataType: 'json', success: function(data){ $.fn.catSelect.onSuccess(data); // zapisanie do cache $.fn.catSelect.cache[id] = data; } }); }
Wszystko to sprowadza się do wywołania funkcji $.fn.catSelect.onSuccess
$.fn.catSelect.onSuccess = function(data){ // usunięcie zbędnych kolumn $.fn.catSelect.obj.nextAll('.catSelect').remove(); if(data.length > 0){ // sa podkategorie - dodanie selecta var select = $(document.createElement('select')) .attr('size', $.fn.catSelect.settings.size) .addClass('catSelect') .change(function(){ // event handler zmian wyboru kategorii $.fn.catSelect.obj = $(this); $.fn.catSelect.onChange(); }); // utworzenie listy podkategori for(i in data){ select.append('<option value="'+data[i].id+'">'+data[i].name+'</option>') } // dodanie selecta do kontenera $.fn.catSelect.settings.container.append(select); // usunięcie zaznaczenie z selecta select.val(0); } else { // nie ma podkategorii - dodanie wartosc do inputa $.fn.catSelect.settings.input.val($.fn.catSelect.obj.val()); } // schowanie zbędnych kolumn if($.fn.catSelect.settings.columns != 0){ var cols = $.fn.catSelect.settings.container.children('select.catSelect').show(); if(cols.length > $.fn.catSelect.settings.columns){ for(var i=0, c=cols.length-$.fn.catSelect.settings.columns; i<c ; i++){ $(cols[i]).hide(); } } } }
Pozostała już tylko do opisania funkcja $.fn.catSelect.selectById(id)
$.fn.catSelect.selectById = function(id){ $.fn.catSelect.reset(); // reset // sprawdzenie cache if($.fn.catSelect.pCache[id]){ $.fn.catSelect.selectByIdOnSuccess($.fn.catSelect.pCache[id]); } else { // pobranie kategori nadrzednych wraz z wszystkimi podkategoriami $.ajax({ url: $.fn.catSelect.settings.urlParent, type: $.fn.catSelect.settings.type, data: 'id='+id, dataType: 'json', success: function(data){ $.fn.catSelect.selectByIdOnSuccess(data); $.fn.catSelect.pCache[id] = data; } }); } } $.fn.catSelect.selectByIdOnSuccess = function(data){ // zapisanie do cache podkategorii for(i in data){ $.fn.catSelect.cache[data[i].id] = data[i].data; } $.fn.catSelect.onChange() // wyświetlenie wszystkich kolumn for(var i=1,s=null;i<data.length;i++){ s = $.fn.catSelect.settings.container.children(); $(s[s.length-1]).val(data[i].id).change(); } }
Cały plik select.js wygląda tak:
(function($) { $.fn.catSelect = function(options){ var defaults = { urlSub: '', urlParent: '', size: 10, type: 'get', container: $(this), input: null, start: 0 }; $.fn.catSelect.settings = $.extend(defaults, options); $.fn.catSelect.reset(); $.fn.catSelect.settings.input.keyup(function(){ $.fn.catSelect.selectById(this.value); }); if($.fn.catSelect.settings.start != 0){ $.fn.catSelect.selectById($.fn.catSelect.settings.start); } else { $.fn.catSelect.onChange(); } } $.fn.catSelect.cache = []; $.fn.catSelect.pCache = []; $.fn.catSelect.obj = null; $.fn.catSelect.reset = function(){ $.fn.catSelect.settings.container.html(''); $.fn.catSelect.obj = $(document.createElement('select')); } $.fn.catSelect.onChange = function(){ var id = $.fn.catSelect.obj.val() || 0; if($.fn.catSelect.cache[id]){ $.fn.catSelect.onSuccess($.fn.catSelect.cache[id]); } else { $.fn.catSelect.get(id); } } $.fn.catSelect.selectById = function(id){ $.fn.catSelect.reset(); if($.fn.catSelect.pCache[id]){ $.fn.catSelect.selectByIdOnSuccess($.fn.catSelect.pCache[id]); } else { $.ajax({ url: $.fn.catSelect.settings.urlParent, type: $.fn.catSelect.settings.type, data: 'id='+id, dataType: 'json', success: function(data){ $.fn.catSelect.selectByIdOnSuccess(data); $.fn.catSelect.pCache[id] = data; } }); } } $.fn.catSelect.selectByIdOnSuccess = function(data){ for(i in data){ $.fn.catSelect.cache[data[i].id] = data[i].data; } $.fn.catSelect.onChange() for(var i=1,s=null;i<data.length;i++){ s = $.fn.catSelect.settings.container.children(); $(s[s.length-1]).val(data[i].id).change(); } } $.fn.catSelect.onSuccess = function(data){ $.fn.catSelect.obj.nextAll('.catSelect').remove(); if(data.length > 0){ var select = $(document.createElement('select')) .attr('size', $.fn.catSelect.settings.size) .addClass('catSelect') .change(function(){ $.fn.catSelect.obj = $(this); $.fn.catSelect.onChange(); }); for(i in data){ select.append('<option value="'+data[i].id+'">'+data[i].name+'</option>') } $.fn.catSelect.settings.container.append(select); select.val(0); } else { $.fn.catSelect.settings.input.val($.fn.catSelect.obj.val()); } if($.fn.catSelect.settings.columns != 0){ var cols = $.fn.catSelect.settings.container.children('select.catSelect').show(); if(cols.length > $.fn.catSelect.settings.columns){ for(var i=0, c=cols.length-$.fn.catSelect.settings.columns; i<c ; i++){ $(cols[i]).hide(); } } } } $.fn.catSelect.get = function(id){ $.ajax({ url: $.fn.catSelect.settings.urlSub, type: $.fn.catSelect.settings.type, data: 'id='+id, dataType: 'json', success: function(data){ $.fn.catSelect.onSuccess(data); $.fn.catSelect.cache[id] = data; } }); } })(jQuery)
A co po stronie serwera?
Ostatnim elementem systemu jest kod po stronie serwera odpowiedzialny za pobranie z bazy danych odpowiednich kategori.
mysql_connect('localhost', 'root'); mysql_select_db('ps'); if(function_exists('ajax_'.$_GET['func'])) { call_user_func('ajax_'.$_GET['func']); } function ajax_sub() { $data = array(); $res = mysql_query('select * from category where parent_id='.(int)$_GET['id']); while($row = mysql_fetch_assoc($res)) { $data[] = array( 'id' => $row['id'], 'name' => $row['name'], 'parent_id' => $row['parent_id'] ); } echo json_encode($data); } function ajax_parent() { $ret = array(); $ret = getParent((int)$_GET['id']); $ret[] = 0; $ids = array_reverse($ret); $sub = array(); foreach($ids as $id) { $data = array(); $res = mysql_query('select * from category where parent_id='.$id); while($row = mysql_fetch_assoc($res)) { $data[] = array( 'id' => $row['id'], 'name' => $row['name'], 'parent_id' => $row['parent_id'] ); } $sub[] = array( 'id' => $id, 'data' => $data ); } echo json_encode($sub); } function getParent($id) { $ret = array(); $res = mysql_query('select * from category where id='.$id); $row = mysql_fetch_assoc($res); if($row){ $ret[] = (int)$id; if((int)$row['parent_id'] != 0) { $ret = array_merge($ret, getParent($row['parent_id'])); } } return $ret; }
Istotnym elementem jest tutaj funkcja json_encode() dostępna w PHP dopiero od wersji PHP 5 >= 5.2.0.
Integracja z Symfony
Przedstawiony powyżej przykład można łatwo zaimplementować praktycznie wszędzie, na przykład w projekcie opartym na Symfony. Wszystko zawiera się w kilku prostych krokach.
- Zainstalowanie pluginu integrującego jQuery z Symfony
- Skopiowanie pliku select.js do katalogu web/js/jq
- Dopisanie do pliku MY_MODULE/config/view.yml :
myActionSuccess: javascripts: [jq/select]
-
Dodanie do pliku actions.class.php metod:
public function executeAjaxSubcategories() { if(!$this->getRequest()->isXmlHttpRequest()) return sfView::NONE; $c = new Criteria(); $c->add(ProductCategoryPeer::PARENT_ID, $this->getRequestParameter('id')); $categories = ProductCategoryPeer::doSelect($c); $out = array(); foreach ($categories as $category) { $out[] = array( 'id' => $category->getId(), 'name' => $category->getName(), 'parent_id' => $category->getParentId() ); } $this->renderText(json_encode($out)); return sfView::NONE; } public function executeAjaxParent() { if(!$this->getRequest()->isXmlHttpRequest()) return sfView::NONE; $ret = array(); $ret = $this->getParentCategory((int)$this->getRequestParameter('id')); $ret[] = 0; $ids = array_reverse($ret); $out = array(); foreach($ids as $id) { $data = array(); $categories = ProductCategoryPeer::retrieveByParentId($id); foreach ($categories as $category) { $data[] = array( 'id' => $category->getId(), 'name' => $category->getName(), 'parent_id' => $category->getParentId() ); } $out[] = array( 'id' => $id, 'data' => $data ); } $this->renderText(json_encode($out)); return sfView::NONE; } private function getParentCategory($id) { $category = ProductCategoryPeer::retrieveByPK($id); if($category) { $ret[] = (int)$id; if((int)$category->getParentId() != 0) { $ret = array_merge($ret, $this->getParentCategory($category->getParentId())); } } return $ret; }
-
Dodanie do pliku MY_APP/config/routing.yml:
myModuleAjaxSubcategories: url: /ajax/subcategories param: {module: myModule, action: ajaxSubcategories} myModuleAjaxParent: url: /ajax/subcategories param: {module: myModule, action: ajaxParent}
-
Umieszczenie skryptu w templatce:
<div id="selectDiv"></div> <?php echo label_for('categoryId', '') ?> <?php echo input_tag('categoryId', $sf_params->get('categoryId'), 'class=text'); ?> <?php echo form_error('categoryId') ?> <script type="text/javascript"> $(document).ready(function(){ $('#selectDiv').catSelect({ urlSub: '<?php echo url_for('@productAjaxSubcategories') ?>', urlParent: '<?php echo url_for('@productAjaxParent') ?>', input: $('#categoryId'), size: 5, start: <?php echo $sf_params->get('categoryId') ?> }); }); </script>
- Gotowe ;)
Uff... Sporo tego wyszło. Jestem otwarty na wszelkie pytania i propozycje. Postaram się o jakieś live demo. Testowame pod FF 2.0 i IE 6. Jakby ktoś mógł sprawdzić pod resztą przeglądarek byłbym wdzięczny. A tymczasem jest 3:15 - ide spać.
P.S. Plugin powstał podczas słuchania "Alive 2007" Daft Punk ;) (a wpis przy Evanescence)
17 komentarzy
Wszystko fajnie, ale jakbyś nadał każdemu @<input />@owi swoje @id@ i wywaliłbyś brzydkie @onclick@i, to byłoby jeszcze lepiej.
To jest chyba tutaj najmniej istotne ;) Chodziło tylko o pokazanie, jak wywołać funkcje. Ale niech będzie - zaraz zmienie (i tak mialem zmienic pare rzeczy)
Edit: No, to demo już jest :)
No tak, to tylko kosmetyka. :)
Przestrzegam, że takich rzeczy należy używać z głową... Np. na witrynie nvidii zdaje to egzamin, ale już szukając produktów na mediamarkt.pl człowieka potrafi szlag trafić - za każdym razem należy klikać od nowa całą hierarchię kategorii. Dlatego do takiego rozwiązania przydaje się pamiętanie ostatnio otwartej kategorii, wg mnie.
Akurat mm.pl to jedna wielka porażka. :)
To już zależy od programisty i sposobu wykorzystania parametru 'start'
Też fakt. Ładna, acz zupełnie niefunkcjonalna witryna.
W jaki sposób użyć tego plugina aby w ramach jednej strony stworzyć kilka takich selectów, całkowicie rozdzielnych?
Myślę, że różne id kontenerów i akcje powinny załatwić sprawę.
Własnie mam z tym problem. Nie jestem osobą biegłą w JS. Zobacz pod poniższy, jeżeli oczywiście masz czas.
http://www.barbara.eu.org/~quaker/jquery/
A co zrobić, żeby można było wybierać nie tylko kategorie z ostatniego poziomu, ale również kategorie "wyższe"?
Przerobic kod ;]
Witam,
Powiedzcie proszę mistrzowie jak to zintegrować z drupalem?
Witam!
Swietny skrypcik ale mam takie (moze infantylne - nie znam dobrze jquery) pytanie...
Czy da sie zrobic tak aby wszystkie boksy byly widoczne jednoczesnie bez klikania w pierwszym - po prostu zeby na poczatku jeden byl wypelniony a pozostale puste?
Jak to jest, że nie można kur... ściągnąć plików .. ?
Bo serwer jest inteligentny i odpowiada tylko ludziom kulturalnym.
Inna możliwość jest taka, że wpis ma półtora roku a serwer już nie istnieje.
teamon
W języku kulturalnych ludzi nie istnieje słowo "bo". Słowo to istnieje w innym słowniku.