niedziela, 14 czerwca 2015

Efekt skaczącej zawartości okna w Safari na iPad

Jako, że Safari na iPad posiada własne efekty animacyjne, między innymi takie jak skaczące okno podczas przesuwania palcem w prawo/lewo (tzw. bouncing effect), to staje się to irytujące kiedy na stronie mamy slajder, w którym możemy przesuwać zdjęcia właśnie palcem.

Efekt jest taki, że wraz z przesunięciem jednego zdjęcia w slajderze, cała zawartość strony, także jest przesuwana, by następnie wróciła na swoje miejsce.

Jak zapobiec takim efektom? W jQuery mamy zdarzenie takie jak touchmove, w którym możemy wykorzystać przerwanie takiego działania poprzez preventDefault();

Przykład nr 1:
$(document).on('touchmove', function(event) {
 event.preventDefault();
});

Powyższy kod nie wymaga szczegółowego opisu, jest dość prosty.

Ale jak teraz zezwolić na używanie przewijania np. zdjęć palcem? Musimy dodać warunek kiedy ma działać a kiedy nie:

Przykład nr 2:
$(document).on('touchmove', function(event) {
 if ($(event.target).closest('.slider').length == 0) {
  event.preventDefault();
 }
});
Przydatne rozwiązanie:)

wtorek, 9 czerwca 2015

Responsywne tabele

Jako, że RWD stało się już standardem w projektowaniu stron, a budowanie stron na tabelkach jest już mocno nieprofesjonalne, to do prezentowania danych należy używać jednak tabel. Jak zatem zbudować tabelę aby była prawidłowo wyświetlana np. na telefonie?

Rozwiązanie są co najmniej dwa, a istniejącej tabeli zasadniczo nie trzeba przebudowywać. Poniżej zaprezentuje dwa rozwiązania dla poniższej tabeli:

Struktura tabeli:
<table>
    <tr>
        <th>Nagłówek 1</th>
        <th>Nagłówek 2</th>
        <th>Nagłówek 3</th>
    </tr>
    <tr>
        <td>Rząd 1 Kolumna 1</td>
        <td>Rząd 1 Kolumna 2</td>
        <td>Rząd 1 Kolumna 3</td>
    </tr>
    <tr>
        <td>Rząd 2 Kolumna 1</td>
        <td>Rząd 2 Kolumna 2</td>
        <td>Rząd 2 Kolumna 3</td>
    </tr>
    <tr>
        <td>Rząd 3 Kolumna 1</td>
        <td>Rząd 3 Kolumna 2</td>
        <td>Rząd 3 Kolumna 3</td>
    </tr>
</table>
Minimalne style:
table {
    width: 100%;
    border-spacing: 0px;
}

th,
td {
    border: 1px solid grey;
}

th + th,
td + td {
    border-left: 0px;
}

tr > td {
    border-top: 0px;
}
Powyższy kod można przetestować na http://jsfiddle.net/m6q90ncd

Załóżmy, że chcemy aby nasza tabela dopiero dostosowywała się przy rozdzielczości ekranu równej i mniejszej niż 420px. Do tego musimy wykorzystać media queries z CSS3, które są dostępne już w większości przeglądarek.

Media queries:
@media screen and (max-width:420px) {
    /* ... */
}

Na cele przykładu zastosowałem wymuszenie nie łamania tekstu w komórkach poprzez dodanie wartości white-space: nowrap;

Rozwiązanie minimalne

W tym rozwiązaniu potrzebujemy otoczyć naszą tabelę dodatkowym blokiem (o klasie .rwd) i dodanie wartości overflow: scroll;, celem przesuwania tabeli przy zachowaniu jej wyglądu. Takie rozwiązanie świetnie sprawdza się gdy chcemy dać użytkownikowi możliwość przesuwania treści tabel używając palca. Rozwiązanie to można także zastosować gdy np. nie chcemy z jakichś powodów skalować obrazu lub innych elementów.

Przykład ten można obejrzeć na http://jsfiddle.net/m6q90ncd/1
Aby przetestować wystarczy zmniejszyć okno.

Rozwiązanie profesjonalne

Tutaj wykorzystamy zalety CSS3 oraz tzw. pseudoklasy. Kod HTML pozostaje bez zmian, jednak nasz kod CSS trochę rozbudujemy:
table {
    width: 100%;
    border-spacing: 0px;
    white-space: nowrap;
}

th,
td {
    border: 1px solid grey;
}

th + th,
td + td {
    border-left: 0px;
}

tr > td {
    border-top: 0px;
}

@media screen and (max-width:420px) {
    table {
       border-top: 1px solid grey;
       border-left: 1px solid grey;
    }
    
    tr:first-of-type {
        display: none;
    }
    
    td {
        display: block;
        width: 100%;
        border-left: 0px;
    }
    
    td:nth-of-type(n):before {
        display: block;        
        float: left;
        width: 25%;
        border-left: 0px;
        border-right: 1px solid grey;
    }
    
    td:nth-of-type(1):before {
        content: 'Nagłówek 1';
    }
    
    td:nth-of-type(2):before {
        content: 'Nagłówek 2';
    }
    
    td:nth-of-type(3):before {
        content: 'Nagłówek 3';
    }
}
O ile kwestie wartości border możemy sobie pominąć - pozwalają tylko na zachowanie wyglądu tabel, to muszę tutaj wspomnieć o zawartości progu 420px.
Na początku ukrywamy cały wiersz z nagłówkami, następnie dla każdej pierwszej komórki w każdym wierszu używamy pseudoklasy :before do zbudowania nagłówka, by później dla każdej kolumny wstawić treść naszej kolumny.

Przykład zaprezentowano na http://jsfiddle.net/m6q90ncd/2.

Czy przechowywanie tekstu w pliku CSS jest prawidłowe? Co jeśli nagłówki w tabeli są tłumaczone, jak to rozwiązać w CSS'ie? Na pomoc przychodzą nam dynamiczne własności z CSS3. Ale najpierw musimy dodać data-atrybuty do naszej tabeli:
<table>
    <tr>
        <th>Nagłówek 1</th>
        <th>Nagłówek 2</th>
        <th>Nagłówek 3</th>
    </tr>
    <tr>
        <td data-title="Nagłówek 1">Rząd 1 Kolumna 1</td>
        <td data-title="Nagłówek 2">Rząd 1 Kolumna 2</td>
        <td data-title="Nagłówek 3">Rząd 1 Kolumna 3</td>
    </tr>
    <tr>
        <td data-title="Nagłówek 1">Rząd 2 Kolumna 1</td>
        <td data-title="Nagłówek 2">Rząd 2 Kolumna 2</td>
        <td data-title="Nagłówek 3">Rząd 2 Kolumna 3</td>
    </tr>
    <tr>
        <td data-title="Nagłówek 1">Rząd 3 Kolumna 1</td>
        <td data-title="Nagłówek 2">Rząd 3 Kolumna 2</td>
        <td data-title="Nagłówek 3">Rząd 3 Kolumna 3</td>
    </tr>
</table>
by następnie skrócić nieco nasz kod CSS:
/* ... */

@media screen and (max-width:420px) {
    table {
       border-top: 1px solid grey;
       border-left: 1px solid grey;
    }
    
    tr:first-of-type {
        display: none;
    }
    
    td {
        display: block;
        width: 100%;
        border-left: 0px;
    }
    
    td:nth-of-type(n):before {
        content: attr(data-title);
        display: block;        
        float: left;
        width: 25%;
        border-left: 0px;
        border-right: 1px solid grey;
    }
}

Sprawę pobierania nagłówków z HTML'a załatwia nam wartość attr(data-title).

Całość do obejrzenia na http://jsfiddle.net/m6q90ncd/3.

niedziela, 26 października 2014

Manipulowanie załącznikami i obrazami w CakePHP - czyli trochę o miniaturkach

Od jakiegoś już czasu rozwijam swój własny Behavior do manipulowania plikami i obrazami w CakePHP, jednak wcześniej nie został nigdzie tak szczegółowo opisany...
Oficjalne i zarazem zawsze aktualne repozytorium (GIT) znajduje się pod adresem repo.kdev.pl/filebehavior (hostowane przez bitbucket.org) i jest prowadzone w języku angielskim. Na tą chwilę behavior przygotowany jest pod wersję 2.x, aby używać go w wersji 1.x należałoby go lekko zmodyfikować. Natomiast, co do nadchodzącego Cake'a 3.0, to pojawi się niebawem odpowiednia wersja...

Dlaczego Behavior, a nie Komponent lub Helper albo jeszcze jakiś Vendor? Ponieważ, z przyjętego założenia, wzorca MVC oraz zasad Cake'a, komponent służy jako wspomagacz Kontrolera, helper Widoku, a behavior Modelu, natomiast vendor powinien obsługiwać osobny skrypt niekoniecznie zgodny z MVC. Jeśli, więc obrazy i załączniki stanowią treść strony, to powinny być obsługiwane jak inne treści, czyli za pomocą modelu, stąd pomocnik modelu czyli behavior. Tyle pokrótce...

Przejdźmy zatem do konkretów...
Po pobraniu behavior'a, a tym samym jedynego pliku FileBehavior.php i zapisaniu go w katalogu app/Model/Behavior możemy już korzystać z jego funkcjonalności.
Aby podłączyć behavior do naszego modelu musimy skorzystać z publicznej własności modelu $actsAs:
public $actsAs = array(
    'File' => array(
        'attachment'
    )
);

Pamiętać należy, iż w tablicy podajemy nazwę behaviora, bez członu 'behavior', a w tablicy podać nazwy pól jakie chcemy zapisać. Oczywiście nazwa pola odpowiada nazwie pola w formularzu:
$this->Form->input('attachment', array(
 'type' => 'file'
));

Dodamy teraz kilka parametrów:
public $actsAs = array(
    'File' => array(
        'attachment'
            'path' => 'files/attachments',
            'types' => array(
                'application/msword',
                'application/vnd.ms-word'
            ),
            'extensions' => array(
                'doc',
                'docx'
            )
        )
    )
);
W powyższym kodzie wskazaliśmy ścieżkę, gdzie pliki będą zapisywane, ścieżka tyczy się adresu w katalogu app/webroot/files/attachments/. Dodatkowo zostały wskazane jakie typy (mime-type) oraz rozszerzenia plików mają być akceptowane. Warto zauważyć, że behavior celowo nie posiada, żadnej bazy typów i rozszerzeń plików, musimy je zawsze zdeklarować.
W sumie to byłoby na tyle jeśli chodzi o "zwykłe" pliki.

Natomiast jeśli chodzi o zapis i manipulowanie obrazami, to zasada jest podobna, z tym że dodatkowo w naszej tablicy wstawiamy klucz thumbs, jak poniżej:
public $actsAs = array(
    'File' => array(
        'image' => array(
            'path' => 'files/images',
            'thumbs' => array(
                'miniature' => array(
                    'square' => array(100)
                ),
                'wide' => array(
                    'width' => array(350)
                ),
                'fitting' => array(
                    'fit' => array(300, 400, true)
                )
            )
        )
    )
);

Jak wcześniej wspomniałem, behavior nie posiada bazy typów i rozszerzeń plików, jednak w przypadku obrazów taka lista jest założona z powodu wymogów i ograniczeń biblioteki GD.

W tablicy thumbs klucze posłużą nam jako sufiksy do nazw plików. Z tablicy wynika, że przesłany plik z pola image zostanie wygenerowany w trzech postaciach z sufiksami, kolejno: _miniature, _wide i _fitting oraz _original jako plik w oryginalnej postaci.
Każdy z tych sufiksów posiada swoją definicję. Do definicji w jakiej postaci ma zostać wygenerowana tzw. miniaturka służy 6 nazw (metod): width, height, shorter, longer, square oraz fit:
  • Metoda width, posiada jeden parametr (integer) przekazywany w tablicy i służy do dopasowania przesyłanego obrazu i skalowania go do wskazanej szerokości.
  • Metoda height, działa podobnie, jednak skaluje do wskazanej wysokości.
  • Metody shorter oraz longer posiadają jeden parametr (integer) przekazywany w tablicy i służą do dopasowania obrazu, odpowiednio do jego krótszego lub dłuższego boku do wskazanej długości.
  • Metoda square posiada jeden parametr (integer) lub dwa parametry (integer oraz boolean) w przekazywanej tablicy. Pierwszy parametr w obu przypadkach to docelowa długość boku wygenerowanego kwadratu. W przypadku podania tylko pierwszego parametru, obraz zostanie zeskalowany oraz przycięty. W przypadku podania obu parametrów, a drugiego jako true, to obraz zostanie zeskalowany tak, aby cały się zmieścił w kwadracie oraz zostaną dodane tzw. marginesy.
  • Metoda fit działa podobnie do metody square, jednak posiada dwa (integer oraz integer) lub trzy (integer, integer, boolean) parametry. W przypadku podania tylko dwóch parametrów obraz zostanie zeskalowany i przycięty do wskazanych wymiarów (szerokość, wysokość). Natomiast w przypadku podania trzeciego parametru jako true, obraz zostanie tak zeskalowany aby zmieścił się w podanych wymiarach oraz zostaną dodane tzw. marginesy.
Podsumowując, z przykładu powyżej wynika, że zostanie wygenerowany: obraz w kwadracie o boku 100 pikseli z prawdopodobnie uciętym obrazem, obraz o szerokości 350 pikseli i wysokości proporcjonalnej oraz obraz dopasowany do szerokości 300 pikseli oraz wysokości 400 pikseli bez ucinania.

Dodatkową opcją jest znak wodny, który jest często wykorzystywany do oznaczenia zdjęć np. logotypem. Aby dodać znak wodny potrzebujemy klucza w naszej tablicy o nazwie watermark:
public $actsAs = array(
    'File' => array(
        'image' => array(
            'path' => 'files/images',
            'watermark' => 'img/logo-watermark.png',
            'thumbs' => array(
                'fitting' => array(
                    'fit' => array(300, 400),
                    'watermark' => 7
                )
             )
        )
    )
);

Klucz watermark dodany pod kluczem path, służy do wskazania grafiki jaka ma zostać dodana jako znak wodny z folderu app/webroot/img/. Natomiast klucz watermark w definicji miniaturki oznacza położenie znaku wodnego. Odpowiednio: 1 (lewy górny róg), 2 (góra, centrum), 3 (prawy górny róg), 4 (lewy, centrum), 5 (środek), 6 (prawy, centrum), 7 (lewy dolny róg), 8 (dół, centrum), 9 (prawy dolny róg).

To byłoby chyba wszystko:)

Zapraszam do testowania, używania oraz zgłaszania uwag i sugestii na repo.kdev.pl/filebehavior/issues.

niedziela, 9 marca 2014

Internacjonalizacja w CakePHP - HABTM i hasOne

W poprzednim poście o i18n w Cake'u wspomniałem, iż następny artykuł będzie tyczył się problemów pobierania danych przy relacjach hasOne i hasAndBelongsToMany.

Co do pobierania tłumaczonych danych przy relacjach belongsTo i hasMany to zostało to opisane w wątku Internacjonalizacja w CakePHP - ciąg dalszy rozbudowy.

AppModel.php (app/Model/):
public function afterFind($results, $primary = false) {
 // belongsTo...
 // hasMany...

 if (!empty($this->hasOne)) {
  foreach ($this->hasOne as $model => $settings) {
   if (isset($this->{$model}->actsAs['Translate'])) {
    if (!empty($results)) {
     $ids = array();

     foreach ($results as $result) {
      if (isset($result[$model][$this->{$model}->primaryKey])) {
       $ids[] = $result[$model][$this->{$model}->primaryKey];
      }
     }

     if (!empty($ids)) {
      $this->{$model}->actsAs['Translate'][] = $this->{$model}->primaryKey;

      $supplements = $this->{$model}->find('all', array(
       'conditions' => array(
        $model . '.' . $this->{$model}->primaryKey => $ids
       ),
       'fields' => $this->{$model}->actsAs['Translate'],
       'recursive' => -1
      );

      if (!empty($supplements)) {
       foreach ($results as $order => $result) {
        foreach ($supplements as $supplement) {
         if ($supplement[$model][$this->{$model}->primaryKey] == $result[$model][$this->{$model}->primaryKey]) {
          $results[$order][$model] = array_merge($result[$model], $supplement[$model]);
         }
        }
       }
      }
     }
    }
   }
  }
 }

 if (!empty($this->hasAndBelongsToMany)) {
  foreach ($this->hasAndBelongsToMany as $model => $settings) {
   if (isset($this->{$model}->actsAs['Translate'])) {
    foreach ($results as $row => $result) {
     if (!empty($results[$row][$model])) {
      $ids = array();

      foreach ($results[$row][$model] as $result) {
       if (isset($result[$this->{$model}->primaryKey])) {
        $ids[] = $result[$this->{$model}->primaryKey];
       }
      }

      if (!empty($ids)) {
       $this->{$model}->actsAs['Translate'][] = $this->{$model}->primaryKey;

       $supplements = $this->{$model}->find('all', array(
        'conditions' => array(
         $model . '.' . $this->{$model}->primaryKey => $ids
        ),
        'fields' => $this->{$model}->actsAs['Translate'],
        'recursive' => -1
       ));

       if (!empty($supplements)) {
        foreach ($results[$row][$model] as $order => $result) {
         foreach ($supplements as $supplement) {
          if ($supplement[$model][$this->{$model}->primaryKey] == $result[$this->{$model}->primaryKey]) {
           $results[$row][$model][$order] = array_merge($result, $supplement[$model]);
          }
         }
        }
       }
      }
     }
    }
   }
  }
 }

 return $results;
}
Ponownie wykorzystuje metodę afterFind(); do połączenia brakujących treść. Myślę, że szczegółowy opis był mocno wyczerpujący w poprzednim poście, a zasada działania hasOne i hasAndBelongsToMany jest bardzo podobna.

poniedziałek, 23 grudnia 2013

Produkcyjna i deweloperska baza danych w CakePHP

Większość z osób projektujących strony zazwyczaj korzysta z dwóch serwerów i dwóch serwerów bazy danych - deweloperskiego (zwanego też rozwojowym, lokalnym, testowym, itp.) oraz produkcyjnym (czyli tzw. końcowym).

Jako, że prace wciąż wymagają poprawek i testów, to żmudne staje się przełączanie konfiguracji między tymi serwerami.

W tym przypadku zaprezentuje jak poradzić sobie w CakePHP z przełączaniem bazy danych między deweloperską a produkcyjną, za pomocą stanu debugowania.

Config/database.php:
class DATABASE_CONFIG {

 public $default = array(
  'datasource' => 'Database/Mysql',
  'persistent' => false,
  'host' => 'localhost',
  'login' => '*****',
  'password' => '*****',
  'database' => 'app',
  'prefix' => '',
  'encoding' => 'utf8'
 );

 public $development = array(
  'datasource' => 'Database/Mysql',
  'persistent' => false,
  'host' => 'localhost',
  'login' => 'root',
  'password' => 'root',
  'database' => 'app',
  'prefix' => '',
  'encoding' => 'utf8'
 );

 public function __construct() {
  if (Configure::read('debug') !== 0) {
   $this->default = $this->development;
  }
 }
}
Klasa DATABASE_CONFIG jest klasą domyślną w której ustawiamy konfigurację baz danych.
Zmienna $default to tablica ustawień domyślnej bazy danych. To znaczy, iż jeśli w dowolnym miejscu naszej aplikacji nie wskażemy zmiany konfiguracji to ta będzie ładowana.
Natomiast zmienna $development (nazwa dowolna) zawiera ustawienia na moim serwerze lokalnym. Takich zmiennych możemy tworzyć dowolnie wiele.

Jako, że przyjętą zasadą jest to iż stan debugowania naszej aplikacji na serwerze produkcyjnym powinien być ustawiony na 0, to tym sposobem możemy rozpoznać w konstruktorze naszej klasy i ustawić dowolną inną konfigurację przypisując ją do zmiennej $default.

Należy pamiętać, że są także inne rozwiązania zmiany baz danych, jak chociażby zmienna $useDbConfig używana w naszych modelach lub za pomocą klasy ConnectionManager. Te rozwiązania jednak mają lepsze zastosowanie gdy chcemy zmienić bazę w trakcie działania naszej aplikacji.